GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

Tracker_Report_Renderer_Table   F
last analyzed

Complexity

Total Complexity 362

Size/Duplication

Total Lines 1971
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 16
Metric Value
wmc 362
lcom 1
cbo 16
dl 0
loc 1971
rs 0.6314

68 Methods

Rating   Name   Duplication   Size   Complexity  
A setSort() 0 3 1
A setColumns() 0 3 1
A setAggregates() 0 3 1
A storeColumnsInSession() 0 7 4
A getColumnsFromDb() 0 13 4
A getSortDao() 0 3 1
A getColumnsDao() 0 3 1
A getAggregatesDao() 0 3 1
A fetchMatchingNumber() 0 4 1
F fetchTHead() 0 119 21
C fetchAddAggregatesUsedFunctionsValue() 0 50 9
F buildOrderedQuery() 0 116 31
A getFieldFactory() 0 3 1
A getType() 0 3 1
F processRequest() 0 239 60
A canFieldBeExportedToCSV() 0 7 4
A saveColumnsRenderer() 0 12 4
A injectUnsavedColumnsInRendererDB() 0 3 1
A isMultisort() 0 14 4
A getSortIcon() 0 3 2
A getIcon() 0 3 1
A __construct() 0 5 1
A initiateSession() 0 6 1
A delete() 0 5 1
C getSort() 0 51 16
A saveSort() 0 8 3
A saveColumns() 0 8 2
C getColumns() 0 28 7
A saveAggregates() 0 8 3
C getAggregates() 0 35 11
C fetch() 0 36 8
A fetchHeader() 0 16 2
A fetchTable() 0 18 2
B fetchAsArtifactLink() 0 31 6
B getOptionsMenuItems() 0 24 1
A getExportResultURL() 0 13 1
A fetchFormStart() 0 8 1
B fetchWidget() 0 29 2
B fetchSort() 0 30 5
A fetchAddColumn() 0 10 1
A fetchRange() 0 11 1
C fetchNextPrevious() 0 65 9
A getDisabledPagerButton() 0 18 1
A getPagerButton() 0 18 1
A reorderColumnsByRank() 0 13 3
A getTableColumns() 0 21 4
D fetchTBody() 0 109 23
F fetchAggregates() 0 74 15
B fetchAddAggregatesUsedFunctionsHeader() 0 22 4
C fetchAddAggregatesButton() 0 44 8
A getAggregateURL() 0 14 1
C fetchAggregatesExtraColumns() 0 28 8
A formatAggregateResult() 0 13 3
A extractFieldsFromColumns() 0 6 1
A fetchMassChange() 0 21 4
A duplicate() 0 8 1
B exportToXml() 0 28 5
F exportToCSV() 0 81 16
B saveAggregatesRenderer() 0 15 5
A saveRendererProperties() 0 7 2
A saveSortRenderer() 0 8 3
A create() 0 19 2
A update() 0 23 2
A setSession() 0 10 2
A afterSaveObject() 0 5 1
A sortHasUsedField() 0 9 3
A canFieldBeUsedToSort() 0 9 1
B fetchViewButtons() 0 41 5

How to fix   Complexity   

Complex Class

Complex classes like Tracker_Report_Renderer_Table often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Tracker_Report_Renderer_Table, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Copyright (c) Enalean, 2015. All Rights Reserved.
4
 * Copyright (c) Xerox Corporation, Codendi Team, 2001-2009. All rights reserved
5
 *
6
 * This file is a part of Tuleap.
7
 *
8
 * Tuleap is free software; you can redistribute it and/or modify
9
 * it under the terms of the GNU General Public License as published by
10
 * the Free Software Foundation; either version 2 of the License, or
11
 * (at your option) any later version.
12
 *
13
 * Tuleap is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
 * GNU General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU General Public License
19
 * along with Tuleap. If not, see <http://www.gnu.org/licenses/>.
20
 */
21
22
require_once('common/include/Codendi_HTTPPurifier.class.php');
23
24
class Tracker_Report_Renderer_Table extends Tracker_Report_Renderer implements Tracker_Report_Renderer_ArtifactLinkable {
25
26
    const EXPORT_LIGHT = 1;
27
    const EXPORT_FULL  = 0;
28
29
    public $chunksz;
30
    public $multisort;
31
32
    /**
33
     * Constructor
34
     *
35
     * @param int $id the id of the renderer
36
     * @param Report $report the id of the report
37
     * @param string $name the name of the renderer
38
     * @param string $description the description of the renderer
39
     * @param int $rank the rank
40
     * @param int $chnuksz the size of the chunk (Browse X at once)
0 ignored issues
show
Bug introduced by
There is no parameter named $chnuksz. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
41
     * @param bool $multisort use multisort?
42
     */
43
    public function __construct($id, $report, $name, $description, $rank, $chunksz, $multisort) {
44
        parent::__construct($id, $report, $name, $description, $rank);
45
        $this->chunksz   = $chunksz;
46
        $this->multisort = $multisort;
47
    }
48
49
    public function initiateSession() {
50
        $this->report_session = new Tracker_Report_Session($this->report->id);
51
        $this->report_session->changeSessionNamespace("renderers");
52
        $this->report_session->set("{$this->id}.chunksz",   $this->chunksz);
53
        $this->report_session->set("{$this->id}.multisort", $this->multisort);
54
    }
55
56
    /**
57
     * Delete the renderer
58
     */
59
    public function delete() {
60
        $this->getSortDao()->delete($this->id);
61
        $this->getColumnsDao()->delete($this->id);
62
        $this->getAggregatesDao()->deleteByRendererId($this->id);
63
    }
64
65
    protected $_sort;
66
    /**
67
     * @param array $sort
68
     */
69
    public function setSort($sort) {
70
        $this->_sort = $sort;
71
    }
72
    /**
73
     * Get field ids used to (multi)sort results
74
     * @return array [{'field_id' => 12, 'is_desc' => 0, 'rank' => 2}, [...]]
75
     */
76
    public function getSort($store_in_session = true) {
77
        $sort = null;
78
        if ($store_in_session) {
79
            if (isset($this->report_session)) {
80
                $sort = $this->report_session->get("{$this->id}.sort");
81
            }
82
        }
83
84
        if ( $sort ) {
85
                $ff = $this->report->getFormElementFactory();
86
                foreach ($sort as $field_id => $properties) {
87
                    if ($properties) {
88
                        if ($field = $ff->getFormElementById($field_id)) {
89
                            if ($field->userCanRead()) {
90
                                $this->_sort[$field_id] = array(
91
                                       'renderer_id '=> $this->id,
92
                                       'field_id'    => $field_id,
93
                                       'is_desc'     => $properties['is_desc'],
94
                                       'rank'        => $properties['rank'],
95
                                    );
96
                                $this->_sort[$field_id]['field'] = $field;
97
                            }
98
                        }
99
                    }
100
                }
101
        } else if (!isset($this->report_session) || !$this->report_session->hasChanged()){
102
103
            if (!is_array($this->_sort)) {
104
                $ff = $this->getFieldFactory();
105
                $this->_sort = array();
106
                foreach($this->getSortDao()->searchByRendererId($this->id) as $row) {
0 ignored issues
show
Bug introduced by
The expression $this->getSortDao()->sea...ByRendererId($this->id) of type false|object is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
107
                    if ($field = $ff->getUsedFormElementById($row['field_id'])) {
108
                        if ($field->userCanRead()) {
109
                            $this->_sort[$row['field_id']] = $row;
110
                            $this->_sort[$row['field_id']]['field'] = $field;
111
                        }
112
                    }
113
                }
114
            }
115
            $sort = $this->_sort;
116
            if ($store_in_session) {
117
                foreach($sort as $field_id => $properties) {
118
                    $this->report_session->set("{$this->id}.sort.{$field_id}.is_desc", $properties['is_desc']);
119
                    $this->report_session->set("{$this->id}.sort.{$field_id}.rank", $properties['rank']);
120
                }
121
            }
122
        } else {
123
            $this->_sort = array();
124
        }
125
        return $this->_sort;
126
    }
127
    /**
128
     * Adds sort values to database
129
     *
130
     * @param array $sort
131
     */
132
    public function saveSort($sort) {
133
        $dao = $this->getSortDao();
134
        if (is_array($sort)) {
135
            foreach ($sort as $key => $s) {
136
                $dao->create($this->id, $s['field']->id);
137
            }
138
        }
139
    }
140
141
    protected $_columns;
142
    /**
143
     * @param array $cols
144
     */
145
    public function setColumns($cols) {
146
        $this->_columns = $cols;
147
    }
148
    /**
149
     * Adds columns to database
150
     *
151
     * @param array $cols
152
     */
153
    public function saveColumns($cols) {
154
        $dao = $this->getColumnsDao();
155
        $rank = -1;
156
        foreach ($cols as $key => $col) {
157
            $rank ++;
158
            $dao->create($this->id, $col['field']->id, null, $rank);
159
        }
160
    }
161
162
    /**
163
     * Get field ids and width used to display results
164
     * @return array  [{'field_id' => 12, 'width' => 33, 'rank' => 5}, [...]]
165
     */
166
    public function getColumns() {
167
        $session_renderer_table_columns = null;
168
        if (isset($this->report_session)) {
169
            $session_renderer_table_columns = $this->report_session->get("{$this->id}.columns");
170
        }
171
        if ( $session_renderer_table_columns ) {
172
                $columns = $session_renderer_table_columns;
173
                $ff = $this->report->getFormElementFactory();
174
                $this->_columns = array();
175
                foreach ($columns as $key => $column) {
176
                    if ($formElement = $ff->getUsedFormElementFieldById($key)) {
177
                        if ($formElement->userCanRead()) {
178
                            $this->_columns[$key] = array(
179
                                'field'    => $formElement,
180
                                'field_id' => $key,
181
                                'width'    => $column['width'],
182
                                'rank'     => $column['rank'],
183
                                );
184
                        }
185
                    }
186
                }
187
        } else {
188
            if (empty($this->_columns)) {
189
                $this->_columns = $this->getColumnsFromDb();
190
            }
191
        }
192
        return $this->_columns;
193
    }
194
195
    protected $_aggregates;
196
    /**
197
     * @param array $aggs
198
     */
199
    public function setAggregates($aggs) {
200
        $this->_aggregates = $aggs;
201
    }
202
    /**
203
     * Adds aggregates to database
204
     *
205
     * @param array $cols
0 ignored issues
show
Bug introduced by
There is no parameter named $cols. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
206
     */
207
    public function saveAggregates($aggs) {
208
        $dao = $this->getAggregatesDao();
209
        foreach ($aggs as $field_id => $aggregates) {
210
            foreach ($aggregates as $aggregate) {
211
                $dao->create($this->id, $field_id, $aggregate);
212
            }
213
        }
214
    }
215
    public function getAggregates() {
216
        $session_renderer_table_functions = &$this->report_session->get("{$this->id}.aggregates");
217
        if ( $session_renderer_table_functions ) {
218
            $aggregates = $session_renderer_table_functions;
219
            $ff = $this->report->getFormElementFactory();
220
            foreach ($aggregates as $field_id => $aggregates) {
221
                if ($formElement = $ff->getFormElementById($field_id)) {
222
                    if ($formElement->userCanRead()) {
223
                        $this->_aggregates[$field_id] = $aggregates;
224
                    }
225
                }
226
            }
227
        } else {
228
            if (empty($this->_aggregates)) {
229
                $ff = $this->getFieldFactory();
230
                $this->_aggregates = array();
231
                foreach($this->getAggregatesDao()->searchByRendererId($this->id) as $row) {
0 ignored issues
show
Bug introduced by
The expression $this->getAggregatesDao(...ByRendererId($this->id) of type false|object is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
232
                    if ($field = $ff->getUsedFormElementById($row['field_id'])) {
233
                        if ($field->userCanRead()) {
234
                            if (!isset($this->_aggregates[$row['field_id']])) {
235
                                $this->_aggregates[$row['field_id']] = array();
236
                            }
237
                            $this->_aggregates[$row['field_id']][] = $row;
238
                        }
239
                    }
240
                }
241
            }
242
            $aggregates = $this->_aggregates;
243
            foreach($aggregates as $field_id => $agg) {
244
                $this->report_session->set("{$this->id}.aggregates.{$field_id}", $agg);
245
            }
246
247
        }
248
        return $this->_aggregates;
249
    }
250
251
    public function storeColumnsInSession() {
252
        $columns = $this->_columns;
253
        foreach($columns as $key => $column) {
254
            $this->report_session->set("{$this->id}.columns.{$key}.width", isset($column['width']) ? $column['width'] : 0);
255
            $this->report_session->set("{$this->id}.columns.{$key}.rank", isset($column['rank']) ? $column['rank'] : 0);
256
        }
257
    }
258
259
     /**
260
     * Get field ids and width used to display results
261
     * @return array  [{'field_id' => 12, 'width' => 33, 'rank' => 5}, [...]]
262
     */
263
    public function getColumnsFromDb() {
264
        $ff = $this->getFieldFactory();
265
        $this->_columns = array();
266
        foreach($this->getColumnsDao()->searchByRendererId($this->id) as $row) {
0 ignored issues
show
Bug introduced by
The expression $this->getColumnsDao()->...ByRendererId($this->id) of type false|object is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
267
            if ($field = $ff->getUsedFormElementFieldById($row['field_id'])) {
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $field is correct as $ff->getUsedFormElementF...dById($row['field_id']) (which targets Tracker_FormElementFacto...dFormElementFieldById()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
268
                if ($field->userCanRead()) {
269
                    $this->_columns[$row['field_id']] = $row;
270
                    $this->_columns[$row['field_id']]['field'] = $field;
271
                }
272
            }
273
        }
274
        return $this->_columns;
275
    }
276
277
    protected function getSortDao() {
278
        return new Tracker_Report_Renderer_Table_SortDao();
279
    }
280
281
    protected function getColumnsDao() {
282
        return new Tracker_Report_Renderer_Table_ColumnsDao();
283
    }
284
285
    protected function getAggregatesDao() {
286
        return new Tracker_Report_Renderer_Table_FunctionsAggregatesDao();
287
    }
288
289
    /**
290
     * Fetch content of the renderer
291
     * @return string
292
     */
293
    public function fetch($matching_ids, $request, $report_can_be_modified, PFUser $user) {
294
        $html = '';
295
        $total_rows = $matching_ids['id'] ? substr_count($matching_ids['id'], ',') + 1 : 0;
296
        $offset     = (int)$request->get('offset');
297
        if ($offset < 0) {
298
            $offset = 0;
299
        }
300
        if($request->get('renderer')) {
301
            $renderer_data = $request->get('renderer');
302
            if ( isset($renderer_data[$this->id]) && isset($renderer_data[$this->id]['chunksz'])) {
303
                $this->report_session->set("{$this->id}.chunksz", $renderer_data[$this->id]['chunksz']);
304
                $this->report_session->setHasChanged();
305
                $this->chunksz = $renderer_data[$this->id]['chunksz'];
306
            }
307
        }
308
309
        $extracolumn = self::EXTRACOLUMN_MASSCHANGE;
310
        if ((int)$request->get('link-artifact-id')) {
311
            $extracolumn = self::EXTRACOLUMN_LINK;
312
        }
313
314
        $html .= $this->fetchHeader($report_can_be_modified, $user, $total_rows);
315
        $html .= $this->fetchTable($report_can_be_modified, $matching_ids, $total_rows, $offset, $extracolumn);
316
317
        //Display next/previous
318
        $html .= $this->fetchNextPrevious($total_rows, $offset, $report_can_be_modified, (int)$request->get('link-artifact-id'));
319
320
        //Display masschange controls
321
        if ((int)$request->get('link-artifact-id')) {
322
            //TODO
323
        } else {
324
            $html .= $this->fetchMassChange($matching_ids, $total_rows, $offset);
325
        }
326
327
        return $html;
328
    }
329
330
    private function fetchHeader($report_can_be_modified, PFUser $user, $total_rows) {
331
        $html = '';
332
333
        $html .= $this->fetchViewButtons($report_can_be_modified, $user);
334
335
        //Display sort info
336
        $html .= '<div class="tracker_report_renderer_table_information">';
337
        if ($report_can_be_modified) {
338
            $html .= $this->fetchSort();
339
        }
340
341
        $html .= $this->fetchMatchingNumber($total_rows);
342
        $html .= '</div>';
343
344
        return $html;
345
    }
346
347
    private function fetchTable($report_can_be_modified, $matching_ids, $total_rows, $offset, $extracolumn) {
348
        $html = '';
349
350
        //Display the head of the table
351
        if ($report_can_be_modified) {
352
            $only_one_column = null;
353
            $with_sort_links = true;
354
        } else {
355
            $only_one_column = null;
356
            $with_sort_links = false;
357
        }
358
        $html .= $this->fetchTHead($extracolumn, $only_one_column, $with_sort_links);
359
360
        //Display the body of the table
361
        $html .= $this->fetchTBody($matching_ids, $total_rows, $offset, $extracolumn);
362
363
        return $html;
364
    }
365
366
    /**
367
     * Fetch content of the renderer
368
     * @return string
369
     */
370
    public function fetchAsArtifactLink($matching_ids, $field_id, $read_only, $prefill_removed_values, $only_rows = false, $from_aid = null) {
371
        $html = '';
372
        $total_rows = $matching_ids['id'] ? substr_count($matching_ids['id'], ',') + 1 : 0;
373
        $offset     = 0;
374
        $use_data_from_db = true;
375
        $extracolumn     = $read_only ? self::NO_EXTRACOLUMN : self::EXTRACOLUMN_UNLINK;
376
        $with_sort_links = false;
377
        $only_one_column = null;
378
        $pagination      = false;
379
        $read_only       = true;
380
        $store_in_session = true;
381
        $head = '';
382
383
        //Display the head of the table
384
        $suffix = '_'. $field_id .'_'. $this->report->id .'_'. $this->id;
385
        $head .= $this->fetchTHead($extracolumn, $only_one_column, $with_sort_links, $use_data_from_db, $suffix);
386
        if (!$only_rows) {
387
            $html .= $head;
388
        }
389
        //Display the body of the table
390
        $html .= $this->fetchTBody($matching_ids, $total_rows, $offset, $extracolumn, $only_one_column, $use_data_from_db, $pagination, $field_id, $prefill_removed_values, $only_rows, $read_only, $store_in_session, $from_aid);
391
392
        if (!$only_rows) {
393
            $html .= $this->fetchArtifactLinkGoToTracker();
394
        }
395
396
        if ($only_rows) {
397
            return array('head' => $head, 'rows' => $html);
398
        }
399
        return $html;
400
    }
401
402
    /**
403
     * Get the item of the menu options.
404
     *
405
     * If no items is returned, the menu won't be displayed.
406
     *
407
     * @return array of 'item_key' => {url: '', icon: '', label: ''}
408
     */
409
    public function getOptionsMenuItems() {
410
        $my_items = array('export' => '');
411
        $my_items['export'] .= '<div class="btn-group">';
412
        $my_items['export'] .= '<a class="btn btn-mini dropdown-toggle" data-toggle="dropdown" href="#">';
413
        $my_items['export'] .= '<i class="icon-download-alt"></i> ';
414
        $my_items['export'] .= $GLOBALS['Language']->getText('plugin_tracker_report', 'export');
415
        $my_items['export'] .= ' <span class="caret"></span>';
416
        $my_items['export'] .= '</a>';
417
        $my_items['export'] .= '<ul class="dropdown-menu">';
418
        $my_items['export'] .= '<li>';
419
        $my_items['export'] .= '<a href="'. $this->getExportResultURL(self::EXPORT_LIGHT) .'">';
420
        $my_items['export'] .= $GLOBALS['Language']->getText('plugin_tracker_include_report', 'export_only_report_columns');
421
        $my_items['export'] .= '</a>';
422
        $my_items['export'] .= '</li>';
423
        $my_items['export'] .= '<li>';
424
        $my_items['export'] .= '<a href="'. $this->getExportResultURL(self::EXPORT_FULL) .'">';
425
        $my_items['export'] .= $GLOBALS['Language']->getText('plugin_tracker_include_report', 'export_all_columns');
426
        $my_items['export'] .= '</a>';
427
        $my_items['export'] .= '</li>';
428
        $my_items['export'] .= '</ul>';
429
        $my_items['export'] .= '</div>';
430
431
        return $my_items + parent::getOptionsMenuItems();
432
    }
433
434
    private function getExportResultURL($export_only_displayed_fields) {
435
        return TRACKER_BASE_URL.'/?'.http_build_query(
436
            array(
437
                'report'         => $this->report->id,
438
                'renderer'       => $this->id,
439
                'func'           => 'renderer',
440
                'renderer_table' => array(
441
                    'export'                       => 1,
442
                    'export_only_displayed_fields' => $export_only_displayed_fields,
443
                ),
444
            )
445
        );
446
    }
447
448
    private function fetchFormStart($id = '', $func = 'renderer') {
449
        $html  = '';
450
        $html .= '<form method="POST" action="" id="'. $id .'" class="form-inline">';
451
        $html .= '<input type="hidden" name="report" value="'. $this->report->id .'" />';
452
        $html .= '<input type="hidden" name="renderer" value="'. $this->id .'" />';
453
        $html .= '<input type="hidden" name="func" value="'.$func.'" />';
454
        return $html;
455
    }
456
457
    /**
458
     * Fetch content to be displayed in widget
459
     */
460
    public function fetchWidget(PFUser $user) {
461
        $html = '';
462
        $use_data_from_db = true;
463
        $store_in_session = false;
464
        $matching_ids = $this->report->getMatchingIds(null, $use_data_from_db);
465
        $total_rows   = $matching_ids['id'] ? substr_count($matching_ids['id'], ',') + 1 : 0;
466
        $offset = 0;
467
        $extracolumn            = self::NO_EXTRACOLUMN;
468
        $with_sort_links        = false;
469
        $only_one_column        = null;
470
        $pagination             = true;
471
        $artifactlink_field_id  = null;
472
        $prefill_removed_values = null;
473
        $only_rows              = false;
474
        $read_only              = true;
475
        $id_suffix              = '';
476
        //Display the head of the table
477
        $html .= $this->fetchTHead($extracolumn, $only_one_column, $with_sort_links, $use_data_from_db, $id_suffix, $store_in_session);
478
        //Display the body of the table
479
        $html .= $this->fetchTBody($matching_ids, $total_rows, $offset, $extracolumn, $only_one_column, $use_data_from_db, $pagination, $artifactlink_field_id, $prefill_removed_values, $only_rows, $read_only, $store_in_session);
480
481
        //Dispaly range
482
        $offset_last = min($offset + $this->chunksz - 1, $total_rows - 1);
483
        $html .= '<div class="tracker_report_table_pager">';
484
        $html .= $this->fetchRange($offset + 1, $offset_last + 1, $total_rows, $this->fetchWidgetGoToReport());
485
        $html .= '</div>';
486
487
        return $html;
488
    }
489
490
    private function fetchMatchingNumber($total_rows) {
491
        $html = '<p>'. $GLOBALS['Language']->getText('plugin_tracker_include_report', 'matching', $total_rows) .'</p>';
492
        return $html;
493
    }
494
495
    private function fetchSort() {
496
        $html = '';
497
        $html .= '<div class="tracker_report_table_sortby_panel">';
498
        $sort_columns = $this->getSort();
499
        if ($this->sortHasUsedField()) {
500
            $html .= $GLOBALS['Language']->getText('plugin_tracker_report','sort_by');
501
            $html .= ' ';
502
            $ff = $this->getFieldFactory();
503
            $sort = array();
504
            foreach($sort_columns as $row) {
505
                if ($row['field'] && $row['field']->isUsed()) {
506
                    $sort[] = '<a id="tracker_report_table_sort_by_'. $row['field_id'] .'"
507
                                  href="?' .
508
                            http_build_query(array(
509
                                                   'report'                  => $this->report->id,
510
                                                   'renderer'                => $this->id,
511
                                                   'func'                    => 'renderer',
512
                                                   'renderer_table[sort_by]' => $row['field_id'],
513
                                                  )
514
                            ) . '">' .
515
                            $row['field']->getLabel() .
516
                            $this->getSortIcon($row['is_desc']) .
517
                            '</a>';
518
                }
519
            }
520
            $html .= implode(' <i class="icon-angle-right"></i> ', $sort);
521
        }
522
        $html .= '</div>';
523
        return $html;
524
    }
525
526
    private function fetchAddColumn() {
527
        $add_columns_presenter = new Templating_Presenter_ButtonDropdownsMini(
528
            'tracker_report_add_columns_dropdown',
529
            $GLOBALS['Language']->getText('plugin_tracker_report', 'toggle_columns'),
530
            $this->report->getFieldsAsDropdownOptions('tracker_report_add_column', $this->getColumns(), Tracker_Report::TYPE_TABLE)
531
        );
532
        $add_columns_presenter->setIcon('icon-eye-close');
533
534
        return $this->report->getTemplateRenderer()->renderToString('button_dropdowns',  $add_columns_presenter);
535
    }
536
537
    private function fetchRange($from, $to, $total_rows, $additionnal_html) {
538
        $html = '';
539
        $html .= '<span class="tracker_report_table_pager_range">';
540
        $html .= $GLOBALS['Language']->getText('plugin_tracker_include_report','items');
541
        $html .= ' <strong>'. $from .'</strong> – <strong>'. $to .'</strong>';
542
        $html .= ' ' . $GLOBALS['Language']->getText('plugin_tracker_renderer_table','items_range_of') . ' <strong>'. $total_rows .'</strong>';
543
        $html .= $additionnal_html;
544
        $html .= '</span>';
545
546
        return $html;
547
    }
548
549
    private function fetchNextPrevious($total_rows, $offset, $report_can_be_modified, $link_artifact_id = null) {
550
        $html = '';
551
        if ($total_rows) {
552
            $parameters = array(
553
                'report'   => $this->report->id,
554
                'renderer' => $this->id,
555
            );
556
            if ($link_artifact_id) {
557
                $parameters['link-artifact-id'] = (int)$link_artifact_id;
558
                $parameters['only-renderer']    = 1;
559
            }
560
            //offset should be the last parameter to ease the concat later
561
            $parameters['offset'] = '';
562
            $url = '?'. http_build_query($parameters);
563
564
            $chunk  = '<span class="tracker_report_table_pager_chunk">';
565
            $chunk .= $GLOBALS['Language']->getText('plugin_tracker', 'items_per_page');
566
            $chunk .= ' ';
567
            if ($report_can_be_modified) {
568
                $chunk .= '<div class="input-append">';
569
                $chunk .= '<input id="renderer_table_chunksz_input" type="text" name="renderer_table[chunksz]" size="1" maxlength="5" value="'. (int)$this->chunksz.'" />';
570
                $chunk .= '<button type="submit" class="btn">Ok</button> ';
571
                $chunk .= '</div> ';
572
            } else {
573
                $chunk .= (int)$this->chunksz;
574
            }
575
            $chunk .= '</span>';
576
577
            $html .= $this->fetchFormStart('tracker_report_table_next_previous_form');
578
            $html .= '<div class="tracker_report_table_pager">';
579
            if ($total_rows < $this->chunksz) {
580
                $html .= $this->fetchRange(1, $total_rows, $total_rows, $chunk);
581
            } else {
582
                if ($offset > 0) {
583
                    $html .= $this->getPagerButton($url . 0, 'begin');
584
                    $html .= $this->getPagerButton($url . ($offset - $this->chunksz), 'prev');
585
                } else {
586
                    $html .= $this->getDisabledPagerButton('begin');
587
                    $html .= $this->getDisabledPagerButton('prev');
588
                }
589
590
                $offset_last = min($offset + $this->chunksz - 1, $total_rows - 1);
591
                $html .= $this->fetchRange($offset + 1, $offset_last + 1, $total_rows, $chunk);
592
593
                if (($offset + $this->chunksz) < $total_rows) {
594
                    if ($this->chunksz > 0) {
595
                        $offset_end = ($total_rows - ($total_rows % $this->chunksz));
596
                    } else {
597
                        $offset_end = PHP_INT_MAX; //weird! it will take many steps to reach the last page if the user is browsing 0 artifacts at once
598
                    }
599
                    if ($offset_end >= $total_rows) {
600
                        $offset_end -= $this->chunksz;
601
                    }
602
                    $html .= $this->getPagerButton($url . ($offset + $this->chunksz), 'next');
603
                    $html .= $this->getPagerButton($url . $offset_end, 'end');
604
                } else {
605
                    $html .= $this->getDisabledPagerButton('next');
606
                    $html .= $this->getDisabledPagerButton('end');
607
                }
608
            }
609
            $html .= '</div>';
610
            $html .= '</form>';
611
        }
612
        return $html;
613
    }
614
615
    private function getDisabledPagerButton($direction) {
616
        $icons = array(
617
            'begin' => 'icon-double-angle-left',
618
            'end'   => 'icon-double-angle-right',
619
            'prev'  => 'icon-angle-left',
620
            'next'  => 'icon-angle-right',
621
        );
622
        $html  = '';
623
        $html .= '<button
624
            class="btn disabled"
625
            type="button"
626
            title="'. $GLOBALS['Language']->getText('global', $direction) .'"
627
            >';
628
        $html .= '<i class="'. $icons[$direction] .'"></i>';
629
        $html .= '</button> ';
630
631
        return $html;
632
    }
633
634
    private function getPagerButton($url, $direction) {
635
        $icons = array(
636
            'begin' => 'icon-double-angle-left',
637
            'end'   => 'icon-double-angle-right',
638
            'prev'  => 'icon-angle-left',
639
            'next'  => 'icon-angle-right',
640
        );
641
        $html  = '';
642
        $html .= '<a
643
            href="'. $url .'"
644
            class="btn"
645
            title="'. $GLOBALS['Language']->getText('global', $direction) .'"
646
            >';
647
        $html .= '<i class="'. $icons[$direction] .'"></i>';
648
        $html .= '</a> ';
649
650
        return $html;
651
    }
652
653
    protected function reorderColumnsByRank($columns) {
654
655
        $array_rank = array();
656
        foreach($columns as $field_id => $properties) {
657
            $array_rank[$field_id] = $properties['rank'];
658
        }
659
        asort($array_rank);
660
        $columns_sort = array();
661
        foreach ($array_rank as $id => $rank) {
662
            $columns_sort[$id] = $columns[$id];
663
        }
664
        return $columns_sort;
665
    }
666
667
    const NO_EXTRACOLUMN         = 0;
668
    const EXTRACOLUMN_MASSCHANGE = 1;
669
    const EXTRACOLUMN_LINK       = 2;
670
    const EXTRACOLUMN_UNLINK     = 3;
671
672
    private function fetchTHead($extracolumn = 1, $only_one_column = null, $with_sort_links = true, $use_data_from_db = false, $id_suffix = '', $store_in_session = true) {
673
        $current_user = UserManager::instance()->getCurrentUser();
674
675
        $html  = '';
676
        $html .= '<table';
677
        if (!$only_one_column) {
678
            $html .= ' id="tracker_report_table'. $id_suffix .'"  width="100%"';
679
        }
680
681
        $classnames = '';
682
        if ($with_sort_links && ! $current_user->isAnonymous()) {
683
            $classnames .= ' reorderable resizable';
684
        }
685
        $html .= ' class="tracker_report_table table table-striped table-bordered '. $classnames .'"';
686
687
        $html .= '>';
688
        $html .= '<thead>';
689
        $html .= '<tr>';
690
691
        if ($extracolumn) {
692
            $display_extracolumn = true;
693
            $classname = 'tracker_report_table_';
694
            if ($extracolumn === self::EXTRACOLUMN_MASSCHANGE && $this->report->getTracker()->userIsAdmin($current_user)) {
695
                $classname .= 'masschange';
696
            } else if ($extracolumn === self::EXTRACOLUMN_LINK) {
697
                $classname .= 'link';
698
            } else if ($extracolumn === self::EXTRACOLUMN_UNLINK) {
699
                $classname .= 'unlink';
700
            } else {
701
                $display_extracolumn = false;
702
            }
703
704
            if ($display_extracolumn) {
705
                $html .= '<th class="'. $classname .'">&nbsp;</th>';
706
            }
707
        }
708
709
        //the link to the artifact
710
        if (!$only_one_column) {
711
            $html .= '<th></th>';
712
        }
713
714
        $ff = $this->getFieldFactory();
715
        $url = '?'. http_build_query(array(
716
                                           'report'                  => $this->report->id,
717
                                           'renderer'                => $this->id,
718
                                           'func'                    => 'renderer',
719
                                           'renderer_table[sort_by]' => '',
720
                                          )
721
        );
722
        if ($use_data_from_db) {
723
            $all_columns = $this->reorderColumnsByRank($this->getColumnsFromDb());
724
        } else {
725
            $all_columns = $this->reorderColumnsByRank($this->getColumns());
726
        }
727
        if ($only_one_column) {
728
            if (isset($all_columns[$only_one_column])) {
729
                $columns = array($all_columns[$only_one_column]);
730
            } else {
731
                $columns = array(array(
732
                    'width' => 0,
733
                    'field' => $ff->getUsedFormElementById($only_one_column),
734
                ));
735
            }
736
        } else {
737
            $columns = $all_columns;
738
        }
739
        $sort_columns = $this->getSort($store_in_session);
740
741
        $purifier = Codendi_HTMLPurifier::instance();
742
        foreach($columns as $column) {
743
            if ($column['width']) {
744
                $width = 'width="'.$column['width'].'%"';
745
            } else {
746
                $width = '';
747
            }
748
            if ( !empty($column['field']) && $column['field']->isUsed()) {
749
                $html .= '<th class="tracker_report_table_column"
750
                              id="tracker_report_table_column_'. $column['field']->id .'"
751
                              '. $width .'>';
752
                $html .= '<input type="hidden"
753
                                 id="tracker_report_table_column_'. $column['field']->id .'_parent"
754
                                 value="'. $column['field']->parent_id .'" />';
755
756
                $label = $purifier->purify($column['field']->getLabel($this->report));
757
758
                if ($with_sort_links) {
759
                    $sort_url = $url . $column['field']->id;
760
761
                    $html .= '<table width="100%" border="0" cellpadding="0" cellspacing="0"><tbody><tr>';
762
                    $html .= '<td class="tracker_report_table_column_grip">&nbsp;&nbsp;</td>';
763
                    $html .= '<td class="tracker_report_table_column_title">';
764
                    if ($this->canFieldBeUsedToSort($column['field'], $current_user)) {
765
                        $html .= '<a href="'. $sort_url .'">';
766
                        $html .= $label;
767
                        $html .= '</a>';
768
                    } else {
769
                        $html .= $label;
770
                    }
771
                    $html .= '</td>';
772
                    $html .= '<td class="tracker_report_table_column_caret">';
773
                    if (isset($sort_columns[$column['field']->getId()])) {
774
                        $html .= '<a href="'. $sort_url .'">';
775
                        $html .= $this->getSortIcon($sort_columns[$column['field']->getId()]['is_desc']);
776
                        $html .= '</a>';
777
                    }
778
                    $html .= '</td>';
779
                    $html .= '</tr></tbody></table>';
780
781
                } else {
782
                    $html .= $label;
783
                }
784
                $html .= '</th>';
785
            }
786
        }
787
        $html .= '</tr>';
788
        $html .= '</thead>';
789
        return $html;
790
    }
791
792
    public function getTableColumns($only_one_column, $use_data_from_db, $store_in_session = true) {
793
        $columns = array();
794
        if ($use_data_from_db) {
795
            $all_columns = $this->reorderColumnsByRank($this->getColumnsFromDb());
796
        } else {
797
            $all_columns = $this->reorderColumnsByRank($this->getColumns());
798
        }
799
        if ($only_one_column) {
800
            if (isset($all_columns[$only_one_column])) {
801
                $columns = array($all_columns[$only_one_column]);
802
            } else {
803
                $columns = array(array(
804
                    'width' => 0,
805
                    'field' => $this->getFieldFactory()->getUsedFormElementFieldById($only_one_column),
806
                ));
807
            }
808
        } else {
809
            $columns = $all_columns;
810
        }
811
        return $columns;
812
    }
813
814
    /**
815
     * Display the body of the table
816
     *
817
     * @param array $matching_ids           The matching ids to display array('id' => '"1,4,8,10", 'last_matching_ids' => "123,145,178,190")
818
     * @param int   $total_rows             The number of total rows (pagination powwwa)
819
     * @param int   $offset                 The offset of the pagination
820
     * @param int   $extracolumn            Need for an extracolumn? NO_EXTRACOLUMN | EXTRACOLUMN_MASSCHANGE | EXTRACOLUMN_LINK | EXTRACOLUMN_UNLINK. Default is EXTRACOLUMN_MASSCHANGE.
821
     * @param int   $only_one_column        The column (field_id) to display. null if all columns are needed. Default is null
822
     * @param bool  $use_data_from_db       true if we need to retrieve data from the db instead of the session. Default is false.
823
     * @param bool  $pagination             true if we display the pagination. Default is true.
824
     * @param int   $artifactlink_field_id  The artifactlink field id. Needed to display report in ArtifactLink field. Default is null
825
     * @param array $prefill_removed_values Array of artifact_id to pre-check. array(123 => X, 345 => X, ...). Default is null
826
     * @param bool  $only_rows              Display only rows, no aggregates or stuff like that. Default is false.
827
     * @param bool  $read_only              Display the table in read only mode. Default is false.
828
     *
829
     * @return string html
830
     */
831
    private function fetchTBody($matching_ids, $total_rows, $offset, $extracolumn = 1, $only_one_column = null, $use_data_from_db = false, $pagination = true, $artifactlink_field_id = null, $prefill_removed_values = null, $only_rows = false, $read_only = false, $store_in_session = true, $from_aid = null) {
832
        $html = '';
833
        if (!$only_rows) {
834
            $html .= "\n<!-- table renderer body -->\n";
835
            $html .= '<tbody>';
836
            $additional_classname = '';
837
        } else {
838
            $additional_classname = 'additional';
839
        }
840
        if ($total_rows) {
841
842
            $columns = $this->getTableColumns($only_one_column, $use_data_from_db);
843
844
            $extracted_fields = $this->extractFieldsFromColumns($columns);
845
846
            $aggregates = false;
847
848
            $queries = $this->buildOrderedQuery($matching_ids, $extracted_fields, $aggregates, $store_in_session);
849
850
            $dao = new DataAccessObject();
851
            $results = array();
852
            foreach($queries as $sql) {
853
                //Limit
854
                if ($total_rows > $this->chunksz && $pagination) {
855
                    $sql .= " LIMIT ". (int)$offset .", ". (int)$this->chunksz;
856
                }
857
                $results[] = $dao->retrieve($sql);
858
            }
859
            // test if first result is valid (if yes, we consider that others are valid too)
860
            if (!empty($results[0])) {
861
                //extract the first results
862
                $first_result = array_shift($results);
863
                //loop through it
864
                foreach ($first_result as $row) { //id, f1, f2
865
                    //merge the row with the other results
866
                    foreach ($results as $result) {
867
                        //[id, f1, f2] + [id, f3, f4]
868
                        $row = array_merge($row, $result->getRow());
869
                        //row == id, f1, f2, f3, f4...
870
                    }
871
                    $html .= '<tr class="'. $additional_classname .'">';
872
                    $current_user = UserManager::instance()->getCurrentUser();
873
                    if ($extracolumn) {
874
                        $display_extracolumn = true;
875
                        $checked   = '';
876
                        $classname = 'tracker_report_table_';
877
                        if ($extracolumn === self::EXTRACOLUMN_MASSCHANGE && $this->report->getTracker()->userIsAdmin($current_user)) {
878
                            $classname .= 'masschange';
879
                            $name       = 'masschange_aids';
880
                        } else if ($extracolumn === self::EXTRACOLUMN_LINK) {
881
                            $classname .= 'link';
882
                            $name       = 'link-artifact[search]';
883
                        } else if ($extracolumn === self::EXTRACOLUMN_UNLINK) {
884
                            $classname .= 'unlink';
885
                            $name       = 'artifact['. (int)$artifactlink_field_id .'][removed_values]['. $row['id'] .']';
886
                            if (isset($prefill_removed_values[$row['id']])) {
887
                                $checked = 'checked="checked"';
888
                            }
889
                        } else {
890
                            $display_extracolumn = false;
891
                        }
892
893
                        if ($display_extracolumn) {
894
                            $html .= '<td class="'. $classname .'" width="1">';
895
                            $html .= '<span><input type="checkbox" name="'. $name .'[]" value="'. $row['id'] .'" '. $checked .' /></span>';
896
                            $html .= '</td>';
897
                        }
898
                    }
899
                    if (!$only_one_column) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $only_one_column of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
900
                        $params = array(
901
                            'aid' => $row['id']
902
                        );
903
                        if ($from_aid != null) {
904
                            $params['from_aid'] = $from_aid;
905
                        }
906
                        $url = TRACKER_BASE_URL .'/?'. http_build_query($params);
907
908
                        $html .= '<td>';
909
                        $html .= '<a
910
                            class="direct-link-to-artifact"
911
                            href="'. $url .'"
912
                            title="'. $GLOBALS['Language']->getText('plugin_tracker_include_report','show') .' artifact #'. $row['id'] .'">';
913
                        $html .= '<i class="icon-edit"></i>';
914
                        $html .= '</td>';
915
                    }
916
                    foreach($columns as $column) {
917
                        if($column['field']->isUsed()) {
918
                            $field_name = $column['field']->name;
919
                            $value      = isset($row[$field_name]) ? $row[$field_name] : null;
920
                            $html      .= '<td class="tracker_report_table_column_'. $column['field']->id .'">';
921
                            $html      .= $column['field']->fetchChangesetValue($row['id'], $row['changeset_id'], $value, $this->report, $from_aid);
922
                            $html      .= '</td>';
923
                        }
924
                    }
925
                    $html .= '</tr>';
926
                }
927
                if (!$only_rows) {
928
                    $html .= $this->fetchAggregates($matching_ids, $extracolumn, $only_one_column, $columns, $extracted_fields, $use_data_from_db, $read_only);
929
                }
930
            }
931
        } else {
932
            $html .= '<tr class="tracker_report_table_no_result"><td colspan="'. (count($this->getColumns())+2) .'" align="center">'. 'No results' .'</td></tr>';
933
        }
934
        if (!$only_rows) {
935
            $html .= '</tbody>';
936
            $html .= '</table>';
937
        }
938
        return $html;
939
    }
940
941
    public function fetchAggregates($matching_ids, $extracolumn, $only_one_column, $columns, $extracted_fields, $use_data_from_db, $read_only) {
942
        $html = '';
943
944
        //We presume that if EXTRACOLUMN_LINK then it means that we are in the ArtifactLink selector so we force read only mode
945
        if ($extracolumn === self::EXTRACOLUMN_LINK) {
946
            $read_only = true;
947
        }
948
949
        $current_user = UserManager::instance()->getCurrentUser();
950
        //Insert function aggregates
951
        if ($use_data_from_db) {
952
            $aggregate_functions_raw = array($this->getAggregatesDao()->searchByRendererId($this->getId()));
953
        } else {
954
            $aggregate_functions_raw = $this->getAggregates();
955
        }
956
        $aggregates = array();
957
        foreach ($aggregate_functions_raw as $rows) {
958
            if ($rows) {
959
                foreach ($rows as $row) {
960
                    //is the field used as a column?
961
                    if (isset($extracted_fields[$row['field_id']])) {
962
                        if (!isset($aggregates[$row['field_id']])) {
963
                            $aggregates[$row['field_id']] = array();
964
                        }
965
                        $aggregates[$row['field_id']][] = $row['aggregate'];
966
                    }
967
                }
968
            }
969
        }
970
        $queries = $this->buildOrderedQuery($matching_ids, $extracted_fields, $aggregates, '', false);
971
        $dao = new DataAccessObject();
972
        $results = array();
973
        foreach ($queries as $key => $sql) {
974
            if ($key === 'aggregates_group_by') {
975
                foreach ($sql as $k => $s) {
976
                    $results[$k] = $dao->retrieve($s);
977
                }
978
            } else {
979
                if ($dar = $dao->retrieve($sql)) {
980
                    $results = array_merge($results, $dar->getRow());
981
                }
982
            }
983
        }
984
985
        $is_first = true;
986
        $html .= '<tr valign="top" class="tracker_report_table_aggregates">';
987
        $html .= $this->fetchAggregatesExtraColumns($extracolumn, $only_one_column, $current_user);
988
        foreach ($columns as $column) {
989
            $field = $column['field'];
990
            if (! $field->isUsed()) {
991
                continue;
992
            }
993
994
            $html .= '<td class="tracker_report_table_column_'. $field->getId() .'">';
995
            $html .= '<table><thead><tr>';
996
            $html .= $this->fetchAddAggregatesUsedFunctionsHeader($field, $aggregates, $results);
997
            $html .= '<th>';
998
            $html .= $this->fetchAddAggregatesButton($read_only, $field, $current_user, $aggregates, $is_first);
999
            $html .= '</th>';
1000
            $html .= '</tr></thead><tbody><tr>';
1001
            $result = $this->fetchAddAggregatesUsedFunctionsValue($field, $aggregates, $results);
1002
            if (! $result) {
1003
                $html .= '<td></td>';
1004
            }
1005
            $html .= $result;
1006
            $html .= '</tr></tbody></table>';
1007
            $html .= '</td>';
1008
1009
            $is_first = false;
1010
        }
1011
        $html .= '</tr>';
1012
1013
        return $html;
1014
    }
1015
1016
    private function fetchAddAggregatesUsedFunctionsHeader(
1017
        Tracker_FormElement_Field $field,
1018
        array $used_aggregates,
1019
        array $results
1020
    ) {
1021
        if (! isset($used_aggregates[$field->getId()])) {
1022
            return '';
1023
        }
1024
1025
        $html = '';
1026
        foreach ($used_aggregates[$field->getId()] as $function) {
1027
            if (! isset($results[$field->getName() . '_' . $function])) {
1028
                continue;
1029
            }
1030
1031
            $html .= '<th>';
1032
            $html .= $GLOBALS['Language']->getText('plugin_tracker_aggregate', $function);
1033
            $html .= '</th>';
1034
        }
1035
1036
        return $html;
1037
    }
1038
1039
    private function fetchAddAggregatesUsedFunctionsValue(
1040
        Tracker_FormElement_Field $field,
1041
        array $used_aggregates,
1042
        array $results
1043
    ) {
1044
        if (! isset($used_aggregates[$field->getId()])) {
1045
            return '';
1046
        }
1047
1048
        $hp   = Codendi_HTMLPurifier::instance();
1049
        $html = '';
1050
        foreach ($used_aggregates[$field->getId()] as $function) {
1051
            $result_key = $field->getName() . '_' . $function;
1052
            if (! isset($results[$result_key])) {
1053
                continue;
1054
            }
1055
1056
            $result = $results[$result_key];
1057
            $html .= '<td>';
1058
            if (is_a($result, 'DataAccessResult')) {
1059
                if ($row = $result->getRow()) {
1060
                    if (isset($row[$result_key])) {
1061
                        //this case is for multiple selectbox/count
1062
                        $html .= '<label>';
1063
                        $html .= $this->formatAggregateResult($row[$result_key]);
1064
                        $html .= '<label>';
1065
                    } else {
1066
                        foreach ($result as $row) {
1067
                            $html .= '<label>';
1068
                            if ($row['label'] === null) {
1069
                                $html .= '<em>'. $GLOBALS['Language']->getText('global', 'null') .'</em>';
1070
                            } else {
1071
                                $html .= $hp->purify($row['label']);
1072
                            }
1073
                            $html .= ':&nbsp;';
1074
                            $html .= $this->formatAggregateResult($row['value']);
1075
                            $html .= '</label>';
1076
                        }
1077
                    }
1078
                }
1079
            } else {
1080
                $html .= '<label>';
1081
                $html .= $this->formatAggregateResult($result);
1082
                $html .= '<label>';
1083
            }
1084
            $html .= '</td>';
1085
        }
1086
1087
        return $html;
1088
    }
1089
1090
    private function fetchAddAggregatesButton(
1091
        $read_only,
1092
        Tracker_FormElement_Field $field,
1093
        PFUser $current_user,
1094
        array $used_aggregates,
1095
        $is_first
1096
    ) {
1097
        $aggregate_functions = $field->getAggregateFunctions();
1098
1099
        if ($read_only || $current_user->isAnonymous()) {
1100
            return;
1101
        }
1102
1103
        if (! $aggregate_functions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $aggregate_functions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1104
            return;
1105
        }
1106
1107
        $html  = '';
1108
        $html .= '<div class="btn-group">';
1109
        $html .= '<a href="#"
1110
            class="btn btn-mini dropdown-toggle"
1111
            title="'. $GLOBALS['Language']->getText('plugin_tracker_aggregate', 'toggle') .'"
1112
            data-toggle="dropdown">';
1113
        $html .= '<i class="icon-plus"></i> ';
1114
        $html .= '<span class="caret"></span>';
1115
        $html .= '</a>';
1116
        $html .= '<ul class="dropdown-menu '. ($is_first ? '' : 'pull-right') .'">';
1117
        foreach ($aggregate_functions as $function) {
1118
            $is_used = isset($used_aggregates[$field->getId()]) && in_array($function, $used_aggregates[$field->getId()]);
1119
            $url = $this->getAggregateURL($field, $function);
1120
            $html .= '<li>';
1121
            $html .= '<a href="'. $url .'">';
1122
            if ($is_used) {
1123
                $html .= '<i class="icon-ok"></i> ';
1124
            }
1125
            $html .= $GLOBALS['Language']->getText('plugin_tracker_aggregate', $function);
1126
            $html .= '</a>';
1127
            $html .= '</li>';
1128
        }
1129
        $html .= '</ul>';
1130
        $html .= '</div>';
1131
1132
        return $html;
1133
    }
1134
1135
    private function getAggregateURL($field, $function) {
1136
        $field_id = $field->getId();
1137
        $params = array(
1138
            'func'       => 'renderer',
1139
            'report'     => $this->report->getId(),
1140
            'renderer'   => $this->getId(),
1141
            'renderer_table' => array(
1142
                'add_aggregate' => array(
1143
                    $field_id => $function
1144
                )
1145
            )
1146
        );
1147
        return TRACKER_BASE_URL .'/?'. http_build_query($params);
1148
    }
1149
1150
    private function fetchAggregatesExtraColumns($extracolumn, $only_one_column, PFUser $current_user) {
1151
        $html        = '';
1152
        $inner_table = '<table><thead><tr><th></th></tr></thead></table>';
1153
       if ($extracolumn) {
1154
            $display_extracolumn = true;
1155
            $classname = 'tracker_report_table_';
1156
            if ($extracolumn === self::EXTRACOLUMN_MASSCHANGE && $this->report->getTracker()->userIsAdmin($current_user)) {
1157
                $classname .= 'masschange';
1158
            } else if ($extracolumn === self::EXTRACOLUMN_LINK) {
1159
                $classname .= 'link';
1160
            } else if ($extracolumn === self::EXTRACOLUMN_UNLINK) {
1161
                $classname .= 'unlink';
1162
            } else {
1163
                $display_extracolumn = false;
1164
            }
1165
1166
            if ($display_extracolumn) {
1167
                $html .= '<td class="' . $classname . '" width="1">';
1168
                $html .= $inner_table;
1169
                $html .= '</td>';
1170
            }
1171
        }
1172
        if (! $only_one_column) {
1173
            $html .= '<td>'. $inner_table .'</td>';
1174
        }
1175
1176
        return $html;
1177
    }
1178
1179
    protected function formatAggregateResult($value) {
1180
        if (is_numeric($value)) {
1181
            $decimals = 2;
1182
            if (round($value) == $value) {
1183
                $decimals = 0;
1184
            }
1185
            $value = round($value, $decimals);
1186
        } else {
1187
            $value = Codendi_HTMLPurifier::instance()->purify($value);
1188
        }
1189
1190
        return '<span class="tracker_report_table_aggregates_value">'. $value .'</span>';
1191
    }
1192
1193
    /**
1194
     * Extract the fields from columns:
1195
     *
1196
     * @param array $columns [ 0 => { 'field' => F1, 'width' => 40 }, 1 => { 'field' => F2, 'width' => 40 } ]
1197
     *
1198
     * @return array [ F1, F2 ]
1199
     */
1200
    public function extractFieldsFromColumns($columns) {
1201
        $fields = array();
1202
        $f = create_function('$v, $i, $t', '$t["fields"][$v["field"]->getId()] = $v["field"];');
1203
        array_walk($columns, $f, array('fields' => &$fields));
1204
        return $fields;
1205
    }
1206
1207
    /**
1208
     * Build oredered query
1209
     *
1210
     * @param array                       $matching_ids The artifact to display
1211
     * @param Tracker_FormElement_Field[] $fields       The fields to display
1212
     *
1213
     * @return array of sql queries
1214
     */
1215
    protected function buildOrderedQuery($matching_ids, $fields, $aggregates = false, $store_in_session = true) {
1216
        if ($aggregates) {
1217
            $select = " SELECT 1 ";
1218
        } else {
1219
            $select = " SELECT a.id AS id, c.id AS changeset_id ";
1220
        }
1221
1222
        $from   = " FROM tracker_artifact AS a INNER JOIN tracker_changeset AS c ON (c.artifact_id = a.id) ";
1223
        $where  = " WHERE a.id IN (". $matching_ids['id'] .")
1224
                      AND c.id IN (". $matching_ids['last_changeset_id'] .") ";
1225
        if ($aggregates) {
1226
            $group_by = '';
1227
            $ordering = false;
1228
        } else {
1229
            $group_by = ' GROUP BY id ';
1230
            $ordering = true;
1231
        }
1232
1233
        $additionnal_select = array();
1234
        $additionnal_from   = array();
1235
1236
        foreach($fields as $field) {
1237
            if ($field->isUsed()) {
1238
                $sel = false;
1239
                if ($aggregates) {
1240
                    if (isset($aggregates[$field->getId()])) {
1241
                        if ($a = $field->getQuerySelectAggregate($aggregates[$field->getId()])) {
1242
                            $sel = $a['same_query'];
1243
                            if ($sel) {
1244
                                $additionnal_select[] = $sel;
1245
                                $additionnal_from[] = $field->getQueryFromAggregate();
1246
                            }
1247
                        }
1248
                    }
1249
                } else {
1250
                    $sel = $field->getQuerySelect();
1251
                    if ($sel) {
1252
                        $additionnal_select[] = $sel;
1253
                        $additionnal_from[] = $field->getQueryFrom();
1254
                    }
1255
                }
1256
            }
1257
        }
1258
1259
        //build an array of queries (due to mysql max join limit
1260
        $queries = array();
1261
        $sys_server_join = intval($GLOBALS['sys_server_join']) - 3;
1262
        if ($sys_server_join <= 0) { //make sure that the admin is not dumb
1263
            $sys_server_join = 20; //default mysql 60 / 3 (max of 3 joins per field)
1264
        }
1265
1266
        $additionnal_select_chunked = array_chunk($additionnal_select, $sys_server_join);
1267
        $additionnal_from_chunked   = array_chunk($additionnal_from, $sys_server_join);
1268
1269
        //both arrays are not necessary the same size
1270
        $n = max(count($additionnal_select_chunked), count($additionnal_from_chunked));
1271
        for ($i = 0 ; $i < $n ; ++$i) {
1272
1273
            //init the select and the from...
1274
            $inner_select = $select;
1275
            $inner_from   = $from;
1276
1277
            //... and populate them
1278
            if (isset($additionnal_select_chunked[$i]) && count($additionnal_select_chunked[$i])) {
1279
                $inner_select .= ', '. implode(', ', $additionnal_select_chunked[$i]);
1280
            }
1281
            if (isset($additionnal_from_chunked[$i]) && count($additionnal_from_chunked[$i])) {
1282
                $inner_from .= implode(' ', $additionnal_from_chunked[$i]);
1283
            }
1284
1285
            //build the query
1286
            $sql = $inner_select . $inner_from . $where . $group_by;
1287
1288
            //add it to the pool
1289
            $queries[] = $sql;
1290
        }
1291
1292
        //Add group by aggregates
1293
        if ($aggregates) {
1294
            foreach($fields as $field) {
1295
                if ($field->isUsed()) {
1296
                    if (isset($aggregates[$field->getId()])) {
1297
                        if ($a = $field->getQuerySelectAggregate($aggregates[$field->getId()])) {
1298
                            foreach($a['separate_queries'] as $sel) {
1299
                                $queries['aggregates_group_by'][$field->getName() .'_'. $sel['function']] = "SELECT ".
1300
                                    $sel['select'] .
1301
                                    $from .' '. $field->getQueryFromAggregate() .
1302
                                    $where .
1303
                                    ($sel['group_by'] ? " GROUP BY ". $sel['group_by'] : '');
1304
                            }
1305
                        }
1306
                    }
1307
                }
1308
            }
1309
        }
1310
1311
        //only sort if we have 1 query
1312
        // (too complicated to sort on multiple queries)
1313
        if ($ordering && count($queries) === 1) {
1314
            $sort = $this->getSort($store_in_session);
1315
            if ($this->sortHasUsedField($store_in_session)) {
1316
                $order = array();
1317
                foreach($sort as $s) {
1318
                    if ( !empty($s['field']) && $s['field']->isUsed()) {
1319
                        $order[] = $s['field']->getQueryOrderby() .' '. ($s['is_desc'] ? 'DESC' : 'ASC');
1320
                    }
1321
                }
1322
                $queries[0] .= " ORDER BY ". implode(', ', $order);
1323
            }
1324
        }
1325
        if ( empty($queries) ) {
1326
            $queries[] = $select.$from.$where.$group_by;
1327
        }
1328
1329
        return $queries;
1330
    }
1331
1332
    private function fetchMassChange($matching_ids, $total_rows, $offset) {
1333
        $html    = '';
1334
        $tracker = $this->report->getTracker();
1335
        if ($tracker->userIsAdmin()) {
1336
            $nb_art    = $matching_ids['id'] ? substr_count($matching_ids['id'], ',') + 1 : 0;
1337
            $first_row = ($nb_art / $this->chunksz) + $offset;
1338
            $last_row  = $first_row + $this->chunksz;
1339
            $html .= '<form method="POST" action="" id="tracker_report_table_masschange_form">';
1340
            $html .= '<input type="hidden" name="func" value="display-masschange-form" />';
1341
            //build input for masschange all searched art ids
1342
            foreach ( explode(',', $matching_ids['id']) as $id ) {
1343
                $html .= '<input type="hidden" name="masschange_aids_all[]" value="'. $id .'"/>';
1344
            }
1345
            $html .= '<div id="tracker_report_table_masschange_panel">';
1346
            $html .= '<input id="masschange_btn_checked" type="submit" class="btn" name="renderer_table[masschange_checked]" value="'.$GLOBALS['Language']->getText('plugin_tracker_include_report', 'mass_change_checked', $first_row, $last_row) .'" /> ';
1347
            $html .= '<input id="masschange_btn_all" type="submit" class="btn" name="renderer_table[masschange_all]" value="'.$GLOBALS['Language']->getText('plugin_tracker_include_report', 'mass_change_all', $total_rows) .'" />';
1348
            $html .= '</div>';
1349
            $html .= '</form>';
1350
        }
1351
        return $html;
1352
    }
1353
1354
    protected function getFieldFactory() {
1355
        return Tracker_FormElementFactory::instance();
1356
    }
1357
1358
    /**
1359
     * Duplicate the renderer
1360
     */
1361
    public function duplicate($from_renderer, $field_mapping) {
1362
        //duplicate sort
1363
        $this->getSortDao()->duplicate($from_renderer->id, $this->id, $field_mapping);
1364
        //duplicate columns
1365
        $this->getColumnsDao()->duplicate($from_renderer->id, $this->id, $field_mapping);
1366
        //duplicate aggregates
1367
        $this->getAggregatesDao()->duplicate($from_renderer->id, $this->id, $field_mapping);
1368
    }
1369
1370
    public function getType() {
1371
        return self::TABLE;
1372
    }
1373
1374
    /**
1375
     * Process the request
1376
     * @param Request $request
1377
     */
1378
    public function processRequest(TrackerManager $tracker_manager, $request, $current_user) {
1379
        $renderer_parameters = $request->get('renderer_table');
1380
        $this->initiateSession();
1381
        if ($renderer_parameters && is_array($renderer_parameters)) {
1382
            //Update the chunksz parameter
1383
            if (isset($renderer_parameters['chunksz'])) {
1384
                $new_chunksz = abs((int)$renderer_parameters['chunksz']);
1385
                if ($new_chunksz && ($this->chunksz != $new_chunksz)) {
1386
                    $this->report_session->set("{$this->id}.chunksz", $new_chunksz);
1387
                    $this->report_session->setHasChanged();
1388
                    $this->chunksz = $new_chunksz;
1389
                }
1390
            }
1391
1392
            //Add an aggregate function
1393
            if (isset($renderer_parameters['add_aggregate']) && is_array($renderer_parameters['add_aggregate'])) {
1394
                list($field_id, $agg) = each($renderer_parameters['add_aggregate']);
1395
1396
                //Is the field used by the tracker?
1397
                $ff = $this->getFieldFactory();
1398
                if ($field = $ff->getUsedFormElementById($field_id)) {
1399
                    //Has the field already an aggregate function?
1400
                    $aggregates = $this->getAggregates();
1401
                    if (isset($aggregates[$field_id])) {
1402
                        //Yes. Check if it has already the wanted aggregate function
1403
                        $found = false;
1404
                        reset($aggregates[$field_id]);
1405
                        while (!$found && (list($key,$row) = each($aggregates[$field_id]))) {
1406
                            if ($row['aggregate'] === $agg) {
1407
                                $found = true;
1408
                                //remove it (toggle)
1409
                                unset($aggregates[$field_id][$key]);
1410
                                $this->report_session->set("{$this->id}.aggregates.{$field_id}", $aggregates[$field_id]);
1411
                            }
1412
                        }
1413
                        if (!$found) {
1414
                            //Add it
1415
                            $aggregates[$field_id][] = array('renderer_id' => $this->id, 'field_id' => $field_id, 'aggregate' => $agg);
1416
                            $this->report_session->set("{$this->id}.aggregates.{$field_id}", $aggregates[$field_id]);
1417
                        }
1418
                        $this->report_session->setHasChanged();
1419
                        //TODO
1420
                    } else {
1421
                        //No. Add it
1422
                        $this->report_session->set("{$this->id}.aggregates.{$field_id}", array(array('renderer_id' => $this->id, 'field_id' => $field_id, 'aggregate' => $agg)));
1423
                        $this->report_session->setHasChanged();
1424
                    }
1425
                }
1426
            }
1427
1428
            //toggle a sort column
1429
            if (isset($renderer_parameters['sort_by'])) {
1430
                $sort_by = (int)$renderer_parameters['sort_by'];
1431
                if ($sort_by) {
1432
1433
                    //Is the field used by the tracker?
1434
                    $ff = $this->getFieldFactory();
1435
                    if ($field = $ff->getUsedFormElementById($sort_by)) {
1436
                        //Is the field used as a column?
1437
                        $columns = $this->getColumns();
1438
                        if (isset($columns[$sort_by])) {
1439
                            //Is the field already used to sort results?
1440
                            $sort_fields = $this->getSort();
1441
                            if (isset($sort_fields[$sort_by])) {
1442
                                $is_desc = &$this->report_session->get("{$this->id}.sort.{$sort_by}.is_desc");
1443
                                //toggle
1444
                                $desc = 1;
1445
                                if ($is_desc == 1) {
1446
                                    $desc = 0;
1447
                                }
1448
                                $this->report_session->set("{$this->id}.sort.{$sort_by}.is_desc", $desc);
1449
                                $this->report_session->setHasChanged();
1450
                            } else {
1451
                                if (!$this->multisort) {
1452
                                    //Drop existing sort
1453
                                    foreach ($sort_fields as $id => $sort_field) {
1454
                                        $this->report_session->remove("{$this->id}.sort", $id);
1455
                                    }
1456
                                }
1457
                                //Add new sort
1458
                                $this->report_session->set("{$this->id}.sort.{$sort_by}", array ('is_desc' => 0, 'rank' => count($this->report_session->get("{$this->id}.sort")) ));
1459
                                $this->report_session->setHasChanged();
1460
                            }
1461
                        }
1462
                    }
1463
                }
1464
            }
1465
1466
            //Reset sort
1467
            if (isset($renderer_parameters['resetsort'])) {
1468
                //Drop existing sort
1469
                $this->report_session->remove("{$this->id}","sort");
1470
                $this->report_session->setHasChanged();
1471
            }
1472
1473
            //Toggle multisort
1474
            if (isset($renderer_parameters['multisort'])) {
1475
                $sort_fields = $this->getSort();
1476
                list($keep_it,) = each($sort_fields);
1477
                $this->multisort = !$this->multisort;
1478
                $this->report_session->set("{$this->id}.multisort", $this->multisort);
1479
                if (!$this->multisort) {
1480
                    $sort = $this->report_session->get("{$this->id}.sort");
1481
                    foreach($sort as $field_id => $properties) {
1482
                        if ($field_id != $keep_it) {
1483
                            $this->report_session->remove("{$this->id}.sort", $field_id);
1484
                            $this->report_session->setHasChanged();
1485
                        }
1486
                    }
1487
                }
1488
            }
1489
1490
            //Remove column
1491
            if (isset($renderer_parameters['remove-column'])) {
1492
                if ($field_id = (int)$renderer_parameters['remove-column']) {
1493
                    //Is the field used by the tracker?
1494
                    $ff = $this->getFieldFactory();
1495
                    if ($field = $ff->getUsedFormElementById($field_id)) {
1496
                        //Is the field used as a column?
1497
                        $columns = $this->getColumns();
1498
                        if (isset($columns[$field_id])) {
1499
                            //Is the field already used to sort results?
1500
                            $sort_fields = $this->getSort();
1501
                            if (isset($sort_fields[$field_id])) {
1502
                                //remove from session
1503
                                $this->report_session->remove("{$this->id}.sort", $field_id);
1504
                                $this->report_session->setHasChanged();
1505
                            }
1506
                            //remove from session
1507
                            $this->report_session->remove("{$this->id}.columns", $field_id);
1508
                            $this->report_session->setHasChanged();
1509
                        }
1510
                    }
1511
                }
1512
            }
1513
1514
            //Add column
1515
            if (isset($renderer_parameters['add-column'])) {
1516
                if ($field_id = (int)$renderer_parameters['add-column']) {
1517
                    $added = false;
1518
                    //Is the field used by the tracker?
1519
                    $ff = $this->getFieldFactory();
1520
                    if ($field = $ff->getUsedFormElementById($field_id)) {
1521
                        //Is the field used as a column?
1522
                        $columns = $this->getColumns();
1523
                        if (!isset($columns[$field_id])) {
1524
                            $session_table_columns = $this->report_session->get("{$this->id}.columns");
1525
                            $nb_col = count( $session_table_columns );
1526
                            //Update session with new column
1527
                            $this->report_session->set("{$this->id}.columns.{$field_id}", array('width' => 12, 'rank' => $nb_col) );
1528
                            $this->report_session->setHasChanged();
1529
                            $added = true;
1530
                        }
1531
                    }
1532
                    if ($added && $request->isAjax()) {
1533
                        $matching_ids    = $this->report->getMatchingIds();
1534
                        $offset          = (int)$request->get('offset');
1535
                        $extracolumn     = self::NO_EXTRACOLUMN;
1536
                        $total_rows      = $matching_ids['id'] ? substr_count($matching_ids['id'], ',') + 1 : 0;
1537
1538
                        echo $this->fetchTHead($extracolumn, $field_id);
1539
                        echo $this->fetchTBody($matching_ids, $total_rows, $offset, $extracolumn, $field_id);
1540
                    }
1541
                }
1542
            }
1543
1544
            //Reorder columns
1545
            if (isset($renderer_parameters['reorder-column']) && is_array($renderer_parameters['reorder-column'])) {
1546
                list($field_id,$new_position) = each($renderer_parameters['reorder-column']);
1547
                $field_id     = (int)$field_id;
1548
                $field_id_s = $field_id;
1549
                $new_position = (int)$new_position;
1550
                if ($field_id) {
1551
                    //Is the field used by the tracker?
1552
                    $ff = $this->getFieldFactory();
1553
                    if ($field = $ff->getUsedFormElementById($field_id)) {
1554
                        //Is the field used as a column?
1555
                        $columns = $this->getColumns();
1556
                        if (isset($columns[$field_id])) {
1557
                            $columns = &$this->report_session->get("{$this->id}.columns");
1558
                            if ($new_position == '-1') {
1559
                                //beginning
1560
                                foreach ($columns as $id => $properties) {
1561
                                    $columns[$id]['rank'] = $properties['rank'] + 1;
1562
                                }
1563
                                $columns[$field_id]['rank'] = 0;
1564
                            } else if ($new_position == '-2') {
1565
                                //end
1566
                                $max = 0;
1567
                                foreach ($columns as $id => $properties) {
1568
                                    if ($properties['rank'] > $max) {
1569
                                        $max = $properties['rank'];
1570
                                    }
1571
                                }
1572
                                $columns[$field_id]['rank'] = $max + 1;
1573
                            } else {
1574
                                //other case
1575
                                $replaced_rank = $columns[$new_position]['rank'] + 1;   // rank of the element to shift right
1576
                                foreach ($columns as $id => $properties) {
1577
                                    if ($properties['rank'] >= $replaced_rank && $id != $field_id) {
1578
                                       $columns[$id]['rank'] += 1;
1579
                                    }
1580
                                }
1581
                                $columns[$field_id]['rank'] = $replaced_rank;
1582
                            }
1583
                            $this->report_session->setHasChanged();
1584
                        }
1585
                    }
1586
                }
1587
            }
1588
1589
            //Resize column
1590
            if (isset($renderer_parameters['resize-column']) && is_array($renderer_parameters['resize-column'])) {
1591
                $ff = $this->getFieldFactory();
1592
                foreach ($renderer_parameters['resize-column'] as $field_id => $new_width) {
1593
                    $field_id  = (int)$field_id;
1594
                    $new_width = (int)$new_width;
1595
                    if ($field_id) {
1596
                        //Is the field used by the tracker?
1597
                        if ($field = $ff->getUsedFormElementById($field_id)) {
1598
                            //Is the field used as a column?
1599
                            $columns = $this->getColumns();
1600
                            if (isset($columns[$field_id])) {
1601
                                $old_width = $columns[$field_id]['width'];
1602
                                $this->report_session->set("{$this->id}.columns.{$field_id}.width", $new_width);
1603
                                $this->report_session->setHasChanged();
1604
                            }
1605
                        }
1606
                    }
1607
                }
1608
            }
1609
1610
            //export
1611
            if (isset($renderer_parameters['export'])) {
1612
                $only_columns = isset($renderer_parameters['export_only_displayed_fields']) && $renderer_parameters['export_only_displayed_fields'];
1613
                $this->exportToCSV($only_columns);
1614
            }
1615
        }
1616
    }
1617
1618
    /**
1619
     * Transforms Tracker_Renderer into a SimpleXMLElement
1620
     *
1621
     * @param SimpleXMLElement $root the node to which the renderer is attached (passed by reference)
1622
     */
1623
    public function exportToXml(SimpleXMLElement $root, $xmlMapping) {
1624
        parent::exportToXML($root, $xmlMapping);
1625
        $root->addAttribute('chunksz', $this->chunksz);
1626
        if ($this->multisort) {
1627
            $root->addAttribute('multisort', $this->multisort);
1628
        }
1629
        $child = $root->addChild('columns');
1630
        foreach ($this->getColumns() as $key => $col) {
1631
1632
            $child->addChild('field')->addAttribute('REF', array_search($key, $xmlMapping));
1633
        }
1634
        //TODO : add aggregates in XML export
1635
        /*if ($this->getAggregates()) {
1636
            $child = $root->addChild('aggregates');
1637
            foreach ($this->getAggregates() as $field_id => $aggregates) {
1638
                foreach ($aggregates as $aggregate) {
1639
                    $child->addChild('aggregate')->addAttribute('REF', array_search($field_id, $xmlMapping))
1640
                                                 ->addAttribute('function', $aggregate);
1641
                }
1642
            }
1643
        }*/
1644
        if ($this->getSort()) {
1645
            $child = $root->addChild('sort');
1646
            foreach ($this->getSort() as $key => $sort) {
1647
                 $child->addChild('field')->addAttribute('REF', array_search($key, $xmlMapping));
1648
            }
1649
        }
1650
    }
1651
1652
    /**
1653
     * Export results to csv
1654
     *
1655
     * @param bool $only_columns True if we need to export only the displayed columns. False for all the fields.
1656
     *
1657
     * @return void
1658
     */
1659
    protected function exportToCSV($only_columns) {
1660
        $matching_ids = $this->report->getMatchingIds();
1661
        $total_rows   = $matching_ids['id'] ? substr_count($matching_ids['id'], ',') + 1 : 0;
1662
1663
        if ($only_columns) {
1664
            $fields = $this->extractFieldsFromColumns($this->reorderColumnsByRank($this->getColumns()));
1665
        } else {
1666
            $fields = Tracker_FormElementFactory::instance()->getUsedFields($this->report->getTracker());
1667
        }
1668
1669
        $lines = array();
1670
        $head  = array('aid');
1671
        foreach ($fields as $field) {
1672
            if ($this->canFieldBeExportedToCSV($field)) {
1673
                $head[] = $field->getName();
1674
            }
1675
        }
1676
        $lines[] = $head;
1677
1678
        $queries = $this->buildOrderedQuery($matching_ids, $fields);
1679
        $dao = new DataAccessObject();
1680
        $results = array();
1681
        foreach($queries as $sql) {
1682
            $results[] = $dao->retrieve($sql);
1683
        }
1684
1685
1686
        if (!empty($results[0])) {
1687
            $i = 0;
1688
            //extract the first results
1689
            $first_result = array_shift($results);
1690
1691
            //loop through it
1692
            foreach ($first_result as $row) { //id, f1, f2
1693
1694
                //merge the row with the other results
1695
                foreach ($results as $result) {
1696
                    //[id, f1, f2] + [id, f3, f4]
1697
                    $row = array_merge($row, $result->getRow());
1698
                    //row == id, f1, f2, f3, f4...
1699
                }
1700
1701
                //build the csv line
1702
                $line = array();
1703
                $line[] = $row['id'];
1704
                foreach($fields as $field) {
1705
                    if($this->canFieldBeExportedToCSV($field)) {
1706
                        $value  = isset($row[$field->getName()]) ? $row[$field->getName()] : null;
1707
                        $line[] = $field->fetchCSVChangesetValue($row['id'], $row['changeset_id'], $value, $this->report);
1708
                    }
1709
                }
1710
                $lines[] = $line;
1711
            }
1712
1713
            $separator = ",";   // by default, comma.
1714
            $user = UserManager::instance()->getCurrentUser();
1715
            $separator_csv_export_pref = $user->getPreference('user_csv_separator');
1716
            switch ($separator_csv_export_pref) {
1717
            case "comma":
1718
                $separator = ',';
1719
                break;
1720
            case "semicolon":
1721
                $separator = ';';
1722
                break;
1723
            case "tab":
1724
                $separator = chr(9);
1725
                break;
1726
            }
1727
1728
            $http = Codendi_HTTPPurifier::instance();
1729
            $file_name = str_replace(' ', '_', 'artifact_' . $this->report->getTracker()->getItemName());
1730
            header('Content-Disposition: filename='. $http->purify($file_name) .'_'. $this->report->getTracker()->getProject()->getUnixName(). '.csv');
1731
            header('Content-type: text/csv');
1732
            foreach($lines as $line) {
1733
                fputcsv(fopen("php://output", "a"), $line, $separator, '"');
1734
            }
1735
            die();
0 ignored issues
show
Coding Style Compatibility introduced by
The method exportToCSV() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
1736
        } else {
1737
            $GLOBALS['Response']->addFeedback('error', 'Unable to export (too many fields?)');
1738
        }
1739
    }
1740
1741
    private function canFieldBeExportedToCSV(Tracker_FormElement_Field $field) {
1742
        return $field->isUsed()
1743
            && $field->userCanRead()
1744
            && (! is_a($field, 'Tracker_FormElement_Field_ArtifactId')
1745
                || is_a($field, 'Tracker_FormElement_Field_PerTrackerArtifactId')
1746
            );
1747
    }
1748
1749
    /**
1750
     * Save columns in db
1751
     *
1752
     * @param int $renderer_id the id of the renderer
1753
     */
1754
    protected function saveColumnsRenderer($renderer_id) {
1755
        $columns = $this->getColumns();
1756
        $ff = $this->getFieldFactory();
1757
        //Add columns in db
1758
        if (is_array($columns)) {
1759
             foreach($columns as $field_id => $properties) {
1760
                 if ($field = $ff->getUsedFormElementById($field_id)) {
1761
                     $this->getColumnsDao()->create($renderer_id, $field_id, $properties['width'], $properties['rank']);
1762
                 }
1763
             }
1764
        }
1765
    }
1766
1767
    /**
1768
     * Save aggregates in db
1769
     *
1770
     * @param int $renderer_id the id of the renderer
1771
     */
1772
    protected function saveAggregatesRenderer($renderer_id) {
1773
        $aggregates = $this->getAggregates();
1774
        $ff = $this->getFieldFactory();
1775
        //Add columns in db
1776
        if (is_array($aggregates)) {
1777
            $dao = $this->getAggregatesDao();
1778
            foreach($aggregates as $field_id => $aggs) {
1779
                if ($field = $ff->getUsedFormElementById($field_id)) {
1780
                    foreach ($aggs as $agg) {
1781
                        $dao->create($renderer_id, $field_id, $agg['aggregate']);
1782
                    }
1783
                }
1784
            }
1785
        }
1786
    }
1787
1788
    /**
1789
     * Save multisort/chunksz in db
1790
     *
1791
     * @param int $renderer_id the id of the renderer
1792
     */
1793
    protected function saveRendererProperties ($renderer_id) {
1794
        $dao = new Tracker_Report_Renderer_TableDao();
1795
        if (!$dao->searchByRendererId($renderer_id)->getRow()) {
1796
            $dao->create($renderer_id, $this->chunksz);
1797
        }
1798
        $dao->save($renderer_id, $this->chunksz, $this->multisort);
1799
    }
1800
1801
    /**
1802
     * Save sort in db
1803
     *
1804
     * @param int $renderer_id the id of the renderer
1805
     */
1806
    protected function saveSortRenderer($renderer_id) {
1807
        $sort = $this->getSort();
1808
        if (is_array($sort)) {
1809
            foreach ($sort as $field_id => $properties) {
1810
                $this->getSortDao()->create($renderer_id, $field_id, $properties['is_desc'], $properties['rank']);
1811
            }
1812
        }
1813
    }
1814
1815
    /**
1816
     * Create a renderer - add in db
1817
     *
1818
     * @return bool true if success, false if failure
1819
     */
1820
    public function create() {
1821
        $success = true;
1822
        $rrf = Tracker_Report_RendererFactory::instance();
1823
1824
        if ($renderer_id = $rrf->saveRenderer($this->report, $this->name, $this->description, $this->getType())) {
1825
            //columns
1826
            $this->saveColumnsRenderer($renderer_id);
1827
1828
            //aggregates
1829
            $this->saveAggregatesRenderer($renderer_id);
1830
1831
            //MultiSort/Chunksz
1832
            $this->saveRendererProperties($renderer_id);
1833
1834
            //Sort
1835
            $this->saveSortRenderer($renderer_id);
1836
        }
1837
        return $success;
1838
    }
1839
1840
1841
    /**
1842
     * Update the renderer
1843
     *
1844
     * @return bool true if success, false if failure
1845
     */
1846
    public function update() {
1847
        $success = true;
1848
        if ($this->id > 0) {
1849
            //first delete existing columns and sort
1850
            $this->getSortDao()->delete($this->id);
1851
            $this->getColumnsDao()->delete($this->id);
1852
            $this->getAggregatesDao()->deleteByRendererId($this->id);
1853
1854
            //columns
1855
            $this->saveColumnsRenderer($this->id);
1856
1857
            //aggregates
1858
            $this->saveAggregatesRenderer($this->id);
1859
1860
            //MultiSort/Chunksz
1861
            $this->saveRendererProperties($this->id);
1862
1863
            //Sort
1864
            $this->saveSortRenderer($this->id);
1865
1866
        }
1867
        return $success;
1868
    }
1869
1870
    /**
1871
     * Set the session
1872
     *
1873
     */
1874
    public function setSession($renderer_id = null) {
1875
        if(!$renderer_id) {
1876
            $renderer_id = $this->id;
1877
        }
1878
        $this->report_session->set("{$this->id}.name", $this->name);
1879
        $this->report_session->set("{$this->id}.description", $this->description);
1880
        $this->report_session->set("{$this->id}.chunksz", $this->chunksz);
1881
        $this->report_session->set("{$this->id}.multisort", $this->multisort);
1882
        $this->report_session->set("{$this->id}.rank", $this->rank);
1883
    }
1884
1885
    /**
1886
     * Finnish saving renderer to database by creating colunms
1887
     *
1888
     * @param Tracker_Report_Renderer $renderer containing the columns
1889
     */
1890
    public function afterSaveObject($renderer) {
1891
        $renderer->injectUnsavedColumnsInRendererDB($this);
1892
        $this->saveAggregates($renderer->getAggregates());
1893
        $this->saveSort($renderer->getSort());
1894
    }
1895
1896
    public function injectUnsavedColumnsInRendererDB(Tracker_Report_Renderer_Table $renderer) {
1897
        $renderer->saveColumns($this->_columns);
1898
    }
1899
1900
    /**
1901
     *Test if sort contains at least one used field
1902
     *
1903
     * @return bool, true f sort has at least one used field
0 ignored issues
show
Documentation introduced by
The doc-type bool, could not be parsed: Expected "|" or "end of type", but got "," at position 4. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
1904
     */
1905
    public function sortHasUsedField($store_in_session = true) {
1906
        $sort = $this->getSort($store_in_session);
1907
        foreach($sort as $s) {
1908
            if ($s['field']->isUsed()) {
1909
                return true;
1910
            }
1911
        }
1912
        return false;
1913
    }
1914
1915
    /**
1916
     *Test if multisort does not contain unused fields
1917
     *
1918
     *@return bool true if still multisort
1919
     */
1920
    public function isMultisort(){
1921
        $sort = $this->getSort();
1922
        $used = 0;
1923
        foreach($sort as $s) {
1924
            if ($s['field']->isUsed()) {
1925
                $used ++;
1926
            }
1927
        }
1928
        if($used < 2) {
1929
            return false;
1930
        } else {
1931
            return true;
1932
        }
1933
    }
1934
1935
    private function getSortIcon($is_desc) {
1936
        return ' <i class="icon-caret-'. ( $is_desc ? 'down' : 'up' ) .'"></i>';
1937
    }
1938
1939
    private function canFieldBeUsedToSort(Tracker_FormElement_Field $field, PFUser $current_user) {
1940
        $field = $this->getFieldFactory()->getComputedValueFieldByNameForUser(
1941
            $field->tracker_id,
1942
            $field->name,
1943
            $current_user
1944
        );
1945
1946
        return $field === null;
1947
    }
1948
1949
    public function getIcon() {
1950
        return 'icon-list-ul';
1951
    }
1952
1953
    private function fetchViewButtons($report_can_be_modified, PFUser $current_user) {
1954
        $html  = '';
1955
        $html .= '<div id="tracker_report_renderer_view_controls">';
1956
        if ($this->sortHasUsedField()) {
1957
            //reset sort
1958
            $reset_sort_params = array(
1959
                'report'                    => $this->report->id,
1960
                'renderer'                  => $this->id,
1961
                'func'                      => 'renderer',
1962
                'renderer_table[resetsort]' => 1
1963
            );
1964
            $html .= '<div class="btn-group"><a class="btn btn-mini" href="?' . http_build_query($reset_sort_params) .'">'
1965
                . '<i class="icon-reply"></i> '
1966
                . $GLOBALS['Language']->getText('plugin_tracker_report','reset_sort')
1967
                . '</a></div> ';
1968
1969
            //toggle multisort
1970
            $multisort_params = array(
1971
                'report'                    => $this->report->id,
1972
                'renderer'                  => $this->id,
1973
                'func'                      => 'renderer',
1974
                'renderer_table[multisort]' => 1
1975
            );
1976
            $multisort_label = $GLOBALS['Language']->getText('plugin_tracker_report','enable_multisort');
1977
            if ($this->multisort) {
1978
                $multisort_label = $GLOBALS['Language']->getText('plugin_tracker_report','disable_multisort');
1979
            }
1980
            $html .= '<div class="btn-group"><a class="btn btn-mini" href="?' . http_build_query($multisort_params) .'">'
1981
                . '<i class="icon-sort"></i> '
1982
                . $multisort_label
1983
                . '</a></div> ';
1984
1985
        }
1986
1987
        if ($report_can_be_modified && ! $current_user->isAnonymous()) {
1988
            $html .= $this->fetchAddColumn();
1989
        }
1990
        $html .= '</div>';
1991
1992
        return $html;
1993
    }
1994
}
1995