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 |
||
| 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) |
||
|
|
|||
| 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) { |
||
| 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) { |
||
| 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) { |
||
| 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) { |
||
| 202 | /** |
||
| 203 | * Adds aggregates to database |
||
| 204 | * |
||
| 205 | * @param array $cols |
||
| 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) { |
||
| 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() { |
||
| 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() { |
||
| 276 | |||
| 277 | protected function getSortDao() { |
||
| 280 | |||
| 281 | protected function getColumnsDao() { |
||
| 284 | |||
| 285 | protected function getAggregatesDao() { |
||
| 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) { |
||
| 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) { |
||
| 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) { |
||
| 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( |
||
| 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) { |
||
| 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) { |
||
| 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() { |
||
| 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() { |
||
| 1373 | |||
| 1374 | /** |
||
| 1375 | * Process the request |
||
| 1376 | * @param Request $request |
||
| 1377 | */ |
||
| 1378 | public function processRequest(TrackerManager $tracker_manager, $request, $current_user) { |
||
| 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(); |
||
| 1736 | } else { |
||
| 1737 | $GLOBALS['Response']->addFeedback('error', 'Unable to export (too many fields?)'); |
||
| 1738 | } |
||
| 1739 | } |
||
| 1740 | |||
| 1741 | private function canFieldBeExportedToCSV(Tracker_FormElement_Field $field) { |
||
| 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) { |
||
| 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) { |
||
| 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 |
||
| 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(){ |
||
| 1934 | |||
| 1935 | private function getSortIcon($is_desc) { |
||
| 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() { |
||
| 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 |
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
$italyis not defined by the methodfinale(...).The most likely cause is that the parameter was removed, but the annotation was not.