1<?php 2// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project 3// 4// All Rights Reserved. See copyright.txt for details and a complete list of authors. 5// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details. 6// $Id$ 7 8function wikiplugin_pivottable_info() 9{ 10 return [ 11 'name' => tr('Pivot table'), 12 'description' => tr('Create and display data in pivot table for reporting'), 13 'prefs' => ['wikiplugin_pivottable'], 14 'body' => tra('Leave one space in the box below to allow easier editing of current values with the plugin popup helper later on'), 15 'validate' => 'all', 16 'format' => 'html', 17 'iconname' => 'table', 18 'introduced' => '16.1', 19 'params' => [ 20 'data' => [ 21 'name' => tr('Data source'), 22 'description' => tr("For example 'tracker:1' or 'activitystream'"), 23 'required' => true, 24 'default' => 0, 25 'filter' => 'text', 26 'profile_reference' => 'tracker', 27 'separator' => ':', 28 'profile_reference_extra_values' => ['activitystream' => 'Activity Stream'], 29 ], 30 'overridePermissions' => [ 31 'name' => tra('Override item permissions'), 32 'description' => tra('Return all tracker items ignoring permissions to view the corresponding items.'), 33 'since' => '18.1', 34 'required' => false, 35 'filter' => 'alpha', 36 'default' => 'n', 37 'options' => [ 38 ['text' => '', 'value' => ''], 39 ['text' => tra('Yes'), 'value' => 'y'], 40 ['text' => tra('No'), 'value' => 'n'] 41 ] 42 ], 43 'width' => [ 44 'required' => false, 45 'name' => tra('Width'), 46 'description' => tr('Width of pivot table. Units: % or px.'), 47 'since' => '', 48 'filter' => 'word', 49 'default' => '100%', 50 ], 51 'height' => [ 52 'required' => false, 53 'name' => tra('Height'), 54 'description' => tr('Height of pivot table. Units: px'), 55 'since' => '', 56 'filter' => 'word', 57 'default' => '400px', 58 ], 59 'rows' => [ 60 'required' => false, 61 'name' => tra('Pivot table Rows'), 62 'description' => tr('Which field or fields to use as table rows. Leaving blank will remove grouping by table rows. ') . ' ' . tr('Use permanentNames in case of tracker fields.') . ' ' . tr('Separated by colon (:) if more than one.'), 63 'since' => '', 64 'filter' => 'text', 65 'default' => '', 66 'profile_reference' => 'tracker_field', 67 'separator' => ':', 68 ], 69 'cols' => [ 70 'required' => false, 71 'name' => tra('Pivot table Columns'), 72 'description' => tr('Which field or fields to use as table columns. Leaving blank will use the first available field.') . ' ' . tr('Use permanentNames in case of tracker fields.') . ' ' . tr('Separated by colon (:) if more than one.'), 73 'since' => '', 74 'filter' => 'text', 75 'default' => '', 76 'profile_reference' => 'tracker_field', 77 'separator' => ':', 78 ], 79 'heatmapDomain' => [ 80 'required' => false, 81 'name' => tra('Values used to decide what heatmapColors to use.'), 82 'description' => tr(''), 83 'since' => '17', 84 'filter' => 'text', 85 'default' => '', 86 'separator' => ':', 87 ], 88 'heatmapColors' => [ 89 'required' => false, 90 'name' => tra('Color for each heatmapDomain value.'), 91 'description' => tr(''), 92 'since' => '17', 93 'filter' => 'text', 94 'default' => '', 95 'separator' => ':', 96 ], 97 'rendererName' => [ 98 'name' => tr('Renderer Name'), 99 'description' => tr('Display format of data'), 100 'since' => '', 101 'required' => false, 102 'filter' => 'text', 103 'default' => 'Table', 104 'options' => [ 105 ['text' => 'Table', 'value' => 'Table'], 106 ['text' => tra('Table Barchart'), 'value' => 'Table Barchart'], 107 ['text' => tra('Heatmap'), 'value' => 'Heatmap'], 108 ['text' => tra('Row Heatmap'), 'value' => 'Row Heatmap'], 109 ['text' => tra('Col Heatmap'), 'value' => 'Col Heatmap'], 110 ['text' => tra('Line Chart'), 'value' => 'Line Chart'], 111 ['text' => tra('Bar Chart'), 'value' => 'Bar Chart'], 112 ['text' => tra('Overlay Bar Chart'), 'value' => 'Overlay Bar Chart'], 113 ['text' => tra('Stacked Bar Chart'), 'value' => 'Stacked Bar Chart'], 114 ['text' => tra('Relative Bar Chart'), 'value' => 'Relative Bar Chart'], 115 ['text' => tra('Boxplot Chart'), 'value' => 'Boxplot Chart'], 116 ['text' => tra('Horizontal Boxplot Chart'), 'value' => 'Horizontal Boxplot Chart'], 117 ['text' => tra('Area Chart'), 'value' => 'Area Chart'], 118 ['text' => tra('Histogram'), 'value' => 'Histogram'], 119 ['text' => tra('Density Histogram'), 'value' => 'Density Histogram'], 120 ['text' => tra('Percent Histogram'), 'value' => 'Percent Histogram'], 121 ['text' => tra('Probability Histogram'), 'value' => 'Probability Histogram'], 122 ['text' => tra('Density Histogram Horizontal'), 'value' => 'Density Histogram Horizontal'], 123 ['text' => tra('Percent Histogram Horizontal'), 'value' => 'Percent Histogram Horizontal'], 124 ['text' => tra('Probability Histogram Horizontal'), 'value' => 'Probability Histogram Horizontal'], 125 ['text' => tra('Horizontal Histogram'), 'value' => 'Horizontal Histogram'], 126 ['text' => tra('Histogram2D'), 'value' => 'Histogram2D'], 127 ['text' => tra('Density Histogram2D'), 'value' => 'Density Histogram2D'], 128 ['text' => tra('Percent Histogram2D'), 'value' => 'Percent Histogram2D'], 129 ['text' => tra('Probability Histogram2D'), 'value' => 'Probability Histogram2D'], 130 ['text' => tra('Density Histogram2D Horizontal'), 'value' => 'Density Histogram2D Horizontal'], 131 ['text' => tra('Percent Histogram2D Horizontal'), 'value' => 'Percent Histogram2D Horizontal'], 132 ['text' => tra('Probability Histogram2D Horizontal'), 'value' => 'Probability Histogram2D Horizontal'], 133 ['text' => tra('Horizontal Histogram2D'), 'value' => 'Horizontal Histogram2D'], 134 ['text' => tra('Scatter Chart'), 'value' => 'Scatter Chart'], 135 ['text' => tra('Treemap'), 'value' => 'Treemap'] 136 ] 137 ], 138 'aggregatorName' => [ 139 'name' => tr('Aggregator Name'), 140 'description' => tr('Function to apply on the numeric values from the variables selected.'), 141 'since' => '', 142 'required' => false, 143 'filter' => 'text', 144 'default' => 'Count', 145 'options' => [ 146 ['text' => 'Count', 'value' => 'Count'], 147 ['text' => tra('Count Unique Values'), 'value' => 'Count Unique Values'], 148 ['text' => tra('List Unique Values'), 'value' => 'List Unique Values'], 149 ['text' => tra('Sum'), 'value' => 'Sum'], 150 ['text' => tra('Integer Sum'), 'value' => 'Integer Sum'], 151 ['text' => tra('Average'), 'value' => 'Average'], 152 ['text' => tra('Minimum'), 'value' => 'Minimum'], 153 ['text' => tra('Maximum'), 'value' => 'Maximum'], 154 ['text' => tra('Sum over Sum'), 'value' => 'Sum over Sum'], 155 ['text' => tra('80% Upper Bound'), 'value' => '80% Upper Bound'], 156 ['text' => tra('80% Lower Bound'), 'value' => '80% Lower Bound'], 157 ['text' => tra('Sum as Fraction of Total'), 'value' => 'Sum as Fraction of Total'], 158 ['text' => tra('Sum as Fraction of Rows'), 'value' => 'Sum as Fraction of Rows'], 159 ['text' => tra('Sum as Fraction of Columns'), 'value' => 'Sum as Fraction of Columns'], 160 ['text' => tra('Count as Fraction of Total'), 'value' => 'Count as Fraction of Total'], 161 ['text' => tra('Count as Fraction of Rows'), 'value' => 'Count as Fraction of Rows'], 162 ['text' => tra('Count as Fraction of Columns'), 'value' => 'Count as Fraction of Columns'] 163 ] 164 ], 165 'vals' => [ 166 'name' => tr('Values'), 167 'description' => tr('Variable with numeric values or tracker field permNames, on which the formula from the aggregator is applied. It can be left empty if aggregator is related to Counts.') . ' ' . tr('Use permanentNames in case of tracker fields, separated by : in case of multiple fields function.'), 168 'since' => '', 169 'required' => false, 170 'filter' => 'text', 171 'profile_reference' => 'tracker_field', 172 'separator' => ':', 173 ], 174 'inclusions' => [ 175 'name' => tr('Inclusions'), 176 'description' => tr('Filter values for fields in rows or columns. Contains JSON encoded object of arrays of strings.'), 177 'since' => '', 178 'required' => false, 179 'filter' => 'text', 180 ], 181 'menuLimit' => [ 182 'name' => tr('Filter list limit'), 183 'description' => tr('Pivottable menuLimit option override - number of entries to consider the menu list too big when filtering on a particular column or row.'), 184 'since' => '16.2', 185 'required' => false, 186 'filter' => 'digits', 187 ], 188 'aggregateDetails' => [ 189 'name' => tr('Aggregate details'), 190 'description' => tr('When enabled, clicking a table cell will popup all items that were aggregated into that cell. Specify the name of the field or fields to use to display the details separated by colon. Enabled by default. To disable, set contents to an empty string.'), 191 'since' => '16.2', 192 'required' => false, 193 'filter' => 'text', 194 'profile_reference' => 'tracker_field', 195 'separator' => ':', 196 ], 197 'highlightMine' => [ 198 'name' => tra('Highlight my items'), 199 'description' => tra('Highlight owned items\' values in Charts.'), 200 'since' => '16.3', 201 'required' => false, 202 'filter' => 'alpha', 203 'default' => 'n', 204 'options' => [ 205 ['text' => '', 'value' => ''], 206 ['text' => tra('Yes'), 'value' => 'y'], 207 ['text' => tra('No'), 'value' => 'n'] 208 ] 209 ], 210 'highlightGroup' => [ 211 'name' => tra('Highlight my group items'), 212 'description' => tra('Highlight items\' values belonging to one of my groups in Charts.'), 213 'since' => '16.3', 214 'required' => false, 215 'filter' => 'alpha', 216 'default' => 'n', 217 'options' => [ 218 ['text' => '', 'value' => ''], 219 ['text' => tra('Yes'), 'value' => 'y'], 220 ['text' => tra('No'), 'value' => 'n'] 221 ] 222 ], 223 'highlightGroupColors' => [ 224 'required' => false, 225 'name' => tra('Color for each highlighted group items.'), 226 'description' => tr(''), 227 'since' => '18.1', 228 'filter' => 'text', 229 'default' => '', 230 'separator' => ':', 231 ], 232 'xAxisLabel' => [ 233 'name' => tr('xAxis label'), 234 'description' => tr('Override label of horizontal axis when using Chart renderers.'), 235 'since' => '16.3', 236 'required' => false, 237 'filter' => 'text', 238 ], 239 'yAxisLabel' => [ 240 'name' => tr('yAxis label'), 241 'description' => tr('Override label of vertical axis when using Chart renderers.'), 242 'since' => '16.3', 243 'required' => false, 244 'filter' => 'text', 245 ], 246 'chartTitle' => [ 247 'name' => tr('Chart title'), 248 'description' => tr('Override title when using Chart renderers.'), 249 'since' => '16.3', 250 'required' => false, 251 'filter' => 'text', 252 ], 253 'chartHoverBar' => [ 254 'name' => tr('Chart hover bar'), 255 'description' => tr('Display the Chart hover bar or not.'), 256 'since' => '16.3', 257 'required' => false, 258 'filter' => 'alpha', 259 'default' => 'y', 260 'options' => [ 261 ['text' => tra('Yes'), 'value' => 'y'], 262 ['text' => tra('No'), 'value' => 'n'] 263 ] 264 ], 265 'translate' => [ 266 'name' => tr('Translate displayed data'), 267 'description' => tr('Use translated data values for calculations and display.') . ' ' . tr('Default value: No'), 268 'since' => '18.3', 269 'required' => false, 270 'filter' => 'alpha', 271 'default' => 'n', 272 'options' => [ 273 ['text' => '', 'value' => ''], 274 ['text' => tra('No'), 'value' => 'n'], 275 ['text' => tra('Yes'), 'value' => 'y'] 276 ] 277 ] 278 ], 279 ]; 280} 281 282function wikiplugin_pivottable($data, $params) 283{ 284 285 //included globals for permission check 286 global $prefs, $page, $wikiplugin_included_page, $user; 287 288 //checking if vendor files are present 289 if (! file_exists('vendor_bundled/vendor/nicolaskruchten/pivottable/')) { 290 return WikiParser_PluginOutput::internalError(tr('Missing required files, please make sure plugin files are installed at vendor_bundled/vendor/nicolaskruchten/pivottable. <br/><br /> To install, please run composer or download from following url:<a href="https://github.com/nicolaskruchten/pivottable/archive/master.zip" target="_blank">https://github.com/nicolaskruchten/pivottable/archive/master.zip</a>')); 291 } 292 293 static $id = 0; 294 $id++; 295 296 $headerlib = TikiLib::lib('header'); 297 $headerlib->add_cssfile('vendor_bundled/vendor/nicolaskruchten/pivottable/dist/pivot.css'); 298 $headerlib->add_jsfile('vendor_bundled/vendor/nicolaskruchten/pivottable/dist/pivot.js', true); 299 $headerlib->add_jsfile('vendor_bundled/vendor/plotly/plotly.js/dist/plotly-cartesian.min.js', true); 300 $headerlib->add_jsfile('vendor_bundled/vendor/nagarajanchinnasamy/subtotal/dist/subtotal.min.js', true); 301 $headerlib->add_jsfile('lib/jquery_tiki/wikiplugin-pivottable.js', true); 302 303 $lang = substr($prefs['site_language'], 0, 2); 304 if (file_exists('vendor_bundled/vendor/nicolaskruchten/pivottable/dist/pivot.' . $lang . '.js')) { 305 $headerlib->add_jsfile('vendor_bundled/vendor/nicolaskruchten/pivottable/dist/pivot.' . $lang . '.js', true); 306 } 307 308 $translate = (! empty($params['translate']) && $params['translate'] == 'y') ? true : false; 309 310 $smarty = TikiLib::lib('smarty'); 311 $smarty->assign('lang', $lang); 312 313 314 //checking data type 315 if (empty($params['data']) || ! is_array($params['data'])) { 316 return WikiParser_PluginOutput::internalError(tr('Missing data parameter with format: source:ID, e.g. tracker:1')); 317 } 318 $dataType = $params['data'][0]; 319 if ($dataType !== 'activitystream' && $dataType !== 'tracker') { 320 return WikiParser_PluginOutput::internalError(tr('Error data parameter')); 321 } 322 323 if (! empty($params['rendererName'])) { 324 $rendererName = $params['rendererName']; 325 } else { 326 $rendererName = "Table"; 327 } 328 329 if (! empty($params['aggregatorName'])) { 330 $aggregatorName = $params['aggregatorName']; 331 } else { 332 $aggregatorName = "Count"; 333 } 334 335 if (! empty($params['width'])) { 336 $width = $params['width']; 337 } else { 338 $width = "100%"; 339 } 340 341 if (! empty($params['height'])) { 342 $height = $params['height']; 343 } else { 344 $height = "1000px"; 345 } 346 347 if ($dataType === "tracker") { 348 $trackerId = $params['data'][1]; 349 $definition = Tracker_Definition::get($trackerId); 350 if (! $definition) { 351 return WikiParser_PluginOutput::userError(tr('Tracker data source not found.')); 352 } 353 354 $perms = Perms::get(['type' => 'tracker', 'object' => $trackerId]); 355 356 $fields = $definition->getFields(); 357 358 if (! $perms->admin_trackers && $params['overridePermissions'] !== 'y') { 359 $hasFieldPermissions = false; 360 foreach ($fields as $key => $field) { 361 $isHidden = $field['isHidden']; 362 $visibleBy = $field['visibleBy']; 363 364 if ($isHidden != 'n' || ! empty($visibleBy)) { 365 $hasFieldPermissions = true; 366 } 367 368 if ($isHidden == 'c') { 369 // creators can see their own items coming from the search index 370 } elseif ($isHidden == 'y') { 371 // Visible by administrator only 372 unset($fields[$key]); 373 } elseif (! empty($visibleBy)) { 374 // Permission based on visibleBy apply 375 $commonGroups = array_intersect($visibleBy, $perms->getGroups()); 376 if (count($commonGroups) == 0) { 377 unset($fields[$key]); 378 } 379 } 380 } 381 if (! $hasFieldPermissions && ! $perms->view_trackers && ! $definition->isEnabled('userCanSeeOwn') && ! $definition->isEnabled('groupCanSeeOwn') && ! $definition->isEnabled('writerCanModify')) { 382 return WikiParser_PluginOutput::userError(tr('You do not have rights to view tracker data.')); 383 } 384 } 385 386 $fields[] = [ 387 'name' => 'object_id', 388 'permName' => 'object_id', 389 'type' => 't' 390 ]; 391 392 $fields[] = [ 393 'name' => 'object_type', 394 'permName' => 'object_type', 395 'type' => 't' 396 ]; 397 398 $fields[] = [ 399 'name' => 'creation_date', 400 'permName' => 'creation_date', 401 'type' => 'f' 402 ]; 403 $fields[] = [ 404 'name' => 'modification_date', 405 'permName' => 'modification_date', 406 'type' => 'f' 407 ]; 408 $fields[] = [ 409 'name' => 'tracker_status', 410 'permName' => 'tracker_status', 411 'type' => 't' 412 ]; 413 414 $heatmapParams = []; 415 if ($rendererName === 'Heatmap') { 416 $validConfig = ! (empty($params['heatmapDomain']) && empty($params['heatmapColors'])) 417 && is_array($params['heatmapDomain']) 418 && is_array($params['heatmapColors']) 419 && count($params['heatmapDomain']) === count($params['heatmapColors']); 420 421 if ($validConfig) { 422 $heatmapParams = [ 423 'domain' => array_map(floatval, $params['heatmapDomain']), 424 'colors' => $params['heatmapColors'] 425 ]; 426 } 427 428 unset($validConfig); 429 } 430 431 $query = new Search_Query; 432 $query->filterType('trackeritem'); 433 $query->filterContent($trackerId, 'tracker_id'); 434 435 $unifiedsearchlib = TikiLib::lib('unifiedsearch'); 436 if (! empty($params['overridePermissions']) && $params['overridePermissions'] === 'y') { 437 $unifiedsearchlib->initQueryBase($query); 438 $unifiedsearchlib->initQueryPresentation($query); 439 } else { 440 $unifiedsearchlib->initQuery($query); 441 } 442 443 $matches = WikiParser_PluginMatcher::match($data); 444 445 $builder = new Search_Query_WikiBuilder($query); 446 $builder->apply($matches); 447 448 if (! $index = $unifiedsearchlib->getIndex()) { 449 return WikiParser_PluginOutput::userError(tr('Unified search index not found.')); 450 } 451 452 $result = []; 453 foreach ($query->scroll($index) as $row) { 454 $result[] = $row; 455 } 456 $result = Search_ResultSet::create($result); 457 $result->setId('wppivottable-' . $id); 458 459 $resultBuilder = new Search_ResultSet_WikiBuilder($result); 460 $resultBuilder->apply($matches); 461 462 $columnsListed = false; 463 $derivedAttributes = []; 464 $splittedAttributes = []; 465 466 foreach ($matches as $match) { 467 if ($match->getName() == 'display' || $match->getName() == 'column') { 468 $columnsListed = true; 469 } elseif ($match->getName() == 'derivedattribute') { 470 if (preg_match('/name="([^"]+)"/', $match->getArguments(), $match_name) 471 && preg_match('/function="([^"]+)"/', $match->getArguments(), $match_function) 472 && preg_match('/parameters="([^"]*)"/', $match->getArguments(), $match_parameters)) { 473 $derivedattr_name = $match_name[1]; 474 $function_name = $match_function[1]; 475 $function_params = explode(':', $match_parameters[1]); 476 477 if (empty($function_params)) { 478 $function_params = ''; 479 } else { 480 $function_params = '"' . implode('","', $function_params) . '"'; 481 } 482 483 $derivedAttributes[] = sprintf('"%s": %s(%s)', $derivedattr_name, $function_name, $function_params); 484 } 485 } elseif ($match->getName() == 'split') { 486 $parser = new WikiParser_PluginArgumentParser; 487 $arguments = $parser->parse($match->getArguments()); 488 if (! isset($arguments['field'])) { 489 return WikiParser_PluginOutput::userError(tr('Split wiki modifier should specify a field.')); 490 } 491 if (! isset($arguments['separator'])) { 492 $arguments['separator'] = ','; 493 } 494 $arguments['field'] = str_replace('tracker_field_', '', $arguments['field']); 495 $splittedAttributes[] = $arguments; 496 } 497 } 498 499 if ($columnsListed) { 500 $data .= '{display name="object_id"}{display name="object_type"}'; 501 $plugin = new Search_Formatter_Plugin_ArrayTemplate($data); 502 $usedFields = array_keys($plugin->getFields()); 503 foreach ($fields as $key => $field) { 504 if (! in_array('tracker_field_' . $field['permName'], $usedFields) 505 && ! in_array($field['permName'], $usedFields) ) { 506 unset($fields[$key]); 507 } 508 } 509 $fields = array_values($fields); 510 $plugin->setFieldPermNames($fields); 511 } else { 512 $plugin = new Search_Formatter_Plugin_ArrayTemplate(implode("", array_map( 513 function ($f) { 514 if (in_array($f['permName'], ['object_id', 'object_type', 'creation_date', 'modification_date', 'tracker_status'])) { 515 return '{display name="' . $f['permName'] . '" default=" "}'; 516 } elseif ($f['type'] == 'e') { 517 return '{display name="tracker_field_' . $f['permName'] . '" format="categorylist" singleList="y" separator=" "}'; 518 } else { 519 return '{display name="tracker_field_' . $f['permName'] . '" default=" "}'; 520 } 521 }, 522 $fields 523 ))); 524 $plugin->setFieldPermNames($fields); 525 } 526 } 527 528 if ($dataType === "activitystream") { 529 $unifiedsearchlib = TikiLib::lib('unifiedsearch'); 530 $query = new Search_Query; 531 $unifiedsearchlib->initQuery($query); 532 $query->filterType('activity'); 533 534 if ($params['overridePermissions'] === 'y') { 535 $unifiedsearchlib->initQueryBase($query); 536 $unifiedsearchlib->initQueryPresentation($query); 537 } else { 538 $unifiedsearchlib->initQuery($query); 539 } 540 541 $matches = WikiParser_PluginMatcher::match($data); 542 543 $builder = new Search_Query_WikiBuilder($query); 544 $builder->enableAggregate(); 545 $builder->apply($matches); 546 547 $query->setOrder('modification_date_desc'); 548 549 if (! $index = $unifiedsearchlib->getIndex()) { 550 throw new Services_Exception_NotAvailable(tr('Activity stream currently unavailable.')); 551 } 552 553 $result = []; 554 foreach ($query->scroll($index) as $row) { 555 $result[] = $row; 556 } 557 $result = Search_ResultSet::create($result); 558 $result->setId('wppivottable-' . $id); 559 560 $paginationArguments = $builder->getPaginationArguments(); 561 562 $resultBuilder = new Search_ResultSet_WikiBuilder($result); 563 $resultBuilder->setPaginationArguments($paginationArguments); 564 $resultBuilder->apply($matches); 565 566 $fields = []; 567 $fields[] = [ 568 'name' => 'object', 569 'permName' => 'object', 570 'type' => 't' 571 ]; 572 573 $fields[] = [ 574 'name' => 'object_id', 575 'permName' => 'object_id', 576 'type' => 't' 577 ]; 578 579 $fields[] = [ 580 'name' => 'object_type', 581 'permName' => 'object_type', 582 'type' => 't' 583 ]; 584 585 $fields[] = [ 586 'name' => 'modification_date', 587 'permName' => 'modification_date', 588 'type' => 'f' 589 ]; 590 591 $fields[] = [ 592 'name' => 'user', 593 'permName' => 'user', 594 'type' => 't' 595 ]; 596 597 $fields[] = [ 598 'name' => 'event_type', 599 'permName' => 'event_type', 600 'type' => 't' 601 ]; 602 603 $fields[] = [ 604 'name' => 'type', 605 'permName' => 'type', 606 'type' => 't' 607 ]; 608 609 try { 610 $plugin = new Search_Formatter_Plugin_ArrayTemplate(implode("", array_map( 611 function ($f) { 612 $activityArray = [ 613 'object', 614 'object_id', 615 'object_type', 616 'modification_date', 617 'user', 618 'event_type', 619 'type' 620 ]; 621 if (in_array($f['permName'], $activityArray)) { 622 return '{display name="' . $f['permName'] . '" default=" "}'; 623 } 624 }, 625 $fields 626 ))); 627 $plugin->setFieldPermNames($fields); 628 } catch (SmartyException $e) { 629 throw new Services_Exception_NotAvailable($e->getMessage()); 630 } 631 } 632 633 $builder = new Search_Formatter_Builder; 634 $builder->setId('wppivottable-' . $id); 635 $builder->setCount($result->count()); 636 $builder->apply($matches); 637 $builder->setFormatterPlugin($plugin); 638 639 $formatter = $builder->getFormatter(); 640 $entries = $formatter->getPopulatedList($result, false); 641 $entries = $plugin->renderEntries($entries); 642 643 $pivotData = []; 644 foreach ($entries as $entry) { 645 $row = []; 646 foreach ($entry as $fieldName => $value) { 647 if ($entry['object_type'] != 'activity' && $field = $definition->getFieldFromPermName($fieldName)) { 648 // Actual data values 649 if ($translate) { 650 $row[$field['name']] = tra($value); 651 } else { 652 $row[$field['name']] = $value; 653 } 654 } else { 655 // predefined fields (created date, lastmod, etc.) 656 $row[$fieldName] = $value; 657 } 658 } 659 $pivotData[] = $row; 660 } 661 662 foreach ($splittedAttributes as $arguments) { 663 $field = $definition->getFieldFromPermName($arguments['field']); 664 if (empty($field)) { 665 continue; 666 } else { 667 $field = $field['name']; 668 } 669 $separator = $arguments['separator']; 670 $key = 0; 671 while($key < count($pivotData)) { 672 $row = $pivotData[$key]; 673 if (! isset($row[$field])) { 674 $key++; 675 continue; 676 } 677 $splitted = explode($separator, $row[$field]); 678 if (count($splitted) == 1) { 679 $key++; 680 continue; 681 } 682 $replacement = array_map(function($value) use ($row, $field) { 683 return array_merge($row, [$field => ltrim($value)]); 684 }, $splitted); 685 array_splice($pivotData, $key, 1, $replacement); 686 $key += count($replacement); 687 } 688 } 689 690 //translating permName to field name for columns and rows 691 $cols = []; 692 if (! empty($params['cols'])) { 693 foreach ($params['cols'] as $colName) { 694 if ($params['data'][0] !== 'activitystream' && $field = $definition->getFieldFromPermName(trim($colName))) { 695 $cols[] = $field['name']; 696 } else { 697 $cols[] = $colName; 698 } 699 } 700 } elseif (! empty($fields) && empty($params['rows'])) { 701 $cols[] = $fields[0]['name']; 702 } 703 704 $rows = []; 705 if (! empty($params['rows'])) { 706 foreach ($params['rows'] as $rowName) { 707 if ($params['data'][0] !== 'activitystream' && $field = $definition->getFieldFromPermName(trim($rowName))) { 708 $rows[] = $field['name']; 709 } else { 710 $rows[] = $rowName; 711 } 712 } 713 } 714 715 $vals = []; 716 if (! empty($params['vals'])) { 717 foreach ($params['vals'] as $valName) { 718 if ($params['data'][0] !== 'activitystream' && $field = $definition->getFieldFromPermName(trim($valName))) { 719 $vals[] = $field['name']; 720 } else { 721 $vals[] = $valName; 722 } 723 } 724 } 725 726 $inclusions = ! empty($params['inclusions']) ? $params['inclusions'] : '{}'; 727 728 // parsing array to hold permNames mapped with field names for save button 729 // and list of date fields for custom sorting 730 $fieldsArr = []; 731 $dateFields = []; 732 foreach ($fields as $field) { 733 $fieldsArr[$field['name']] = $field['permName']; 734 if ($field['type'] == 'f') { 735 $dateFields[] = $field['name']; 736 } 737 } 738 739 $smarty->loadPlugin('smarty_function_object_link'); 740 741 if (! isset($params['aggregateDetails'])) { 742 if (isset($fields[2])) { 743 $params['aggregateDetails'][] = $fields[2]['permName']; 744 } elseif (isset($fields[0])) { 745 $params['aggregateDetails'][] = $fields[0]['permName']; 746 } 747 } 748 749 if (! empty($params['aggregateDetails']) && ! empty($params['aggregateDetails'][0])) { 750 $aggregateDetails = []; 751 foreach ($params['aggregateDetails'] as $fieldName) { 752 if ($params['data'][0] != 'activitystream' && $field = $definition->getFieldFromPermName(trim($fieldName))) { 753 $aggregateDetails[] = $field['name']; 754 } else { 755 $aggregateDetails[] = trim($fieldName); 756 } 757 } 758 foreach ($pivotData as &$row) { 759 $title = implode(' ', array_map(function ($field) use ($row) { 760 return $row[$field]; 761 }, $aggregateDetails)); 762 $pivotLinkParams = [ 763 'type' => $row['object_type'], 764 'id' => $row['object_id'], 765 'title' => $title, 766 ]; 767 if ($row['object_type'] === 'activity') { 768 $pivotLinkParams = [ 769 'type' => $row['type'], 770 'id' => $row['object'], 771 'title' => $row['type'], 772 ]; 773 } 774 $row['pivotLink'] = smarty_function_object_link($pivotLinkParams, $smarty->getEmptyInternalTemplate()); 775 } 776 } else { 777 $params['aggregateDetails'] = []; 778 } 779 780 $highlight = []; 781 if (! empty($params['highlightMine']) && $params['highlightMine'] === 'y' && $params['data'][0] !== 'activitystream') { 782 $ownerFields = array_map(function ($fieldId) use ($definition) { 783 return $definition->getField($fieldId); 784 }, $definition->getItemOwnerFields()); 785 foreach ($pivotData as $item) { 786 foreach ($ownerFields as $ownerField) { 787 $itemUsers = TikiLib::lib('trk')->parse_user_field(@$item[$ownerField['name']]); 788 if (in_array($user, $itemUsers)) { 789 $highlight[] = ['item' => $item]; 790 break; 791 } 792 } 793 } 794 } 795 if (! empty($params['highlightGroup']) && $params['highlightGroup'] === 'y') { 796 $groupField = null; 797 foreach ($fields as $field) { 798 if ($field['type'] == 'g') { 799 $groupField = $field; 800 break; 801 } 802 } 803 if ($groupField) { 804 $myGroups = TikiLib::lib('tiki')->get_user_groups($user); 805 foreach ($pivotData as $item) { 806 $group = @$item[$groupField['name']]; 807 if (in_array($group, $myGroups)) { 808 $highlight[] = ['item' => $item, 'group' => $group]; 809 } 810 } 811 $groupColors = []; 812 if ($prefs['feature_conditional_formatting'] === 'y') { 813 $groupsInfo = TikiLib::lib('user')->get_group_info($myGroups); 814 foreach ($groupsInfo as $groupInfo) { 815 $groupColors[$groupInfo['groupName']] = $groupInfo['groupColor']; 816 } 817 } 818 if (! empty($params['highlightGroupColors'])) { 819 $index = 0; 820 foreach ($myGroups as $group) { 821 if (empty($params['highlightGroupColors'][$index])) { 822 break; 823 } 824 $groupColors[$group] = $params['highlightGroupColors'][$index]; 825 $index++; 826 } 827 } 828 if ($groupColors) { 829 foreach ($highlight as &$row) { 830 if (! empty($row['group'])) { 831 $group = $row['group']; 832 if ($group && ! empty($groupColors[$group])) { 833 $row['color'] = $groupColors[$group]; 834 } 835 } 836 } 837 } 838 } 839 } 840 841 //checking if user can see edit button 842 if (! empty($wikiplugin_included_page)) { 843 $sourcepage = $wikiplugin_included_page; 844 } else { 845 $sourcepage = $page; 846 } 847 //checking if user has edit permissions on the wiki page using the current permission library to obey global/categ/object perms 848 $objectperms = Perms::get([ 'type' => 'wiki page', 'object' => $sourcepage ]); 849 if ($objectperms->edit) { 850 $showControls = true; 851 } else { 852 $showControls = false; 853 } 854 855 $out = str_replace(['~np~', '~/np~'], '', $formatter->renderFilters()); 856 857 $smarty->assign('pivottable', [ 858 'id' => 'pivottable' . $id, 859 'trows' => $rows, 860 'tcolumns' => $cols, 861 'dataSource' => $dataType == 'activitystream' ? $dataType : $dataType . ':' . $trackerId, 862 'data' => $pivotData, 863 'derivedAttributes' => $derivedAttributes, 864 'rendererName' => $rendererName, 865 'aggregatorName' => $aggregatorName, 866 'vals' => $vals, 867 'width' => $width, 868 'height' => $height, 869 'heatmapParams' => $heatmapParams, 870 'showControls' => $showControls, 871 'page' => $sourcepage, 872 'fieldsArr' => $fieldsArr, 873 'dateFields' => $dateFields, 874 'inclusions' => $inclusions, 875 'menuLimit' => empty($params['menuLimit']) ? null : $params['menuLimit'], 876 'aggregateDetails' => implode(':', $params['aggregateDetails']), 877 'highlight' => $highlight, 878 'highlightMine' => empty($params['highlightMine']) ? null : $params['highlightMine'], 879 'highlightGroup' => empty($params['highlightGroup']) ? null : $params['highlightGroup'], 880 'xAxisLabel' => empty($params['xAxisLabel']) ? null : $params['xAxisLabel'], 881 'yAxisLabel' => empty($params['yAxisLabel']) ? null : $params['yAxisLabel'], 882 'chartTitle' => empty($params['chartTitle']) ? null : $params['chartTitle'], 883 'chartHoverBar' => empty($params['chartHoverBar']) ? null : $params['chartHoverBar'], 884 'translate' => empty($params['translate']) ? null : $params['translate'], 885 'index' => $id 886 ]); 887 888 $out .= $smarty->fetch('wiki-plugins/wikiplugin_pivottable.tpl'); 889 890 return $out; 891} 892