Odyssey
reports.i
1 <?php
2 /**
3  * @package reports.i -- common functions for creating a report based on an SQL and column array.
4  * @author SPB
5  *
6  * Configuration is required. The file that calls this needs to have the global variables:
7  * @param String $defaultDateFormat -- for any date columns, this is the default format. If this is empty and it is empty at the lower level of configuration,
8  * then no format will be applied to dates.
9  * @param Associative Array $reportList -- Each key is a particular report. This value is not used except internally to identify the report.
10  * Properties of report --
11  * String "title" -- This value displays on the report list for that report and in the title of the report page,
12  * SQL "sql" -- valid SQL for getting the report's data. Filter, sort, and paging is added to the initial query,
13  * Associate Array "cols": This is a map. Title of the column => Type of column. Types acceptable: "string", "number", "boolean", "date", "odata", "currency", "list" and "ignore".
14  * cols correspond to the select part of the SQL positionally. The first title, type is applied to the first column in the SQL select statement.
15  * Associate Array "ddl" : Title of the column => array(value => text)
16  * @param Associate Array $reportMenu -- Each key is section of the report menu. Each value is a (Numeric Array) of report keys in that section of the report (corresponds to $reportList).
17  * @param String $dataFile -- refers to the file with the data calls, now $_SERVER["PHP_SELF"] set at the main level.
18 */
19 
20 /**
21  * class PivotCTE
22  * NOTE: Odyssey only. This is in Mammoth to keep this file in sync but it can only be used in Odyssey because Mammoth uses a prior version of Postgres that doesn't have support for Common Table Expressions (CTEs).
23  *
24  * This class represents the CTE for months. It replaces the culogtrack_pivot which has the current month plus the past 12 months. There is a column per month and quarter which is zero or one.
25  */
26 class PivotCTE
27 {
28  /**
29  * @var array $pivotArray -- the data in the culogtrack_pivot table.
30  * @var string $cte -- the WITH clause of the SQL. This doesn't change so after it is calculated, it is stored here.
31  * @var array $columnSQLArray -- this maps the column SQL. If the SQL is already created, use it.
32  */
33  private $pivotArray;
34  private $cte;
35  private $columnSQLArray;
36 
37  /**
38  * function __construct()
39  *
40  * This function gets the current timestamp, strips out everything but the year and the month, and then fills in the pivotArray values based on that. The pivotArray will have 13 records: 12 months ago to the current month.
41  * Each record has the date as varchar Ym, title as "Jan", "Feb", etc., months as an array of zeroes and ones-- the ones will be the month that that date corresponds to,
42  * quarters as an array of zeroes and ones -- the ones will be the quarter that the date corresponds to.
43  */
44  function __construct()
45  {
46  $pivotArray= array();
47  try
48  {
49  $date= new DateTime();
50  $dateText= $date->format("Ym");
51  $date= DateTime::createFromFormat("Ym", $dateText); // Clears out the rest of the date. Just concerned with the year and the month.
52 
53  for($i=0; $i <= 12; $i++)
54  {
55  $row= array("date" => "'" . $date->format("Ym") . "'::varchar", "title" => $date->format("M"), "months" => array_fill(0,13, 0), "quarters" => array_fill(0,4,0));
56  $row["months"][$i]= 1;
57  $row["quarters"][floor($i/4)]= 1;
58  $pivotArray[]= $row;
59  $date->modify("-1 month");
60  }
61  }
62  catch(exception $e)
63  {
64  throw new exception ("Pivot CTE not created correctly: " . $e->getMessage());
65  }
66 
67  $this->pivotArray= $pivotArray;
68  $this->columnSQLArray= array();
69  }
70 
71  /**
72  * public function getCTE()
73  *
74  * This function gets the WITH clause to the SQL. It uses the "values" syntax and gets all the values from the pivotArray. If the CTE is already calculated, then it just returns that.
75  */
76  public function getCTE()
77  {
78  if (isset($this->cte))
79  return $this->cte;
80 
81  $cteArray= array();
82  foreach($this->pivotArray as $pivotRow)
83  {
84  $cteArray[]= "(" . $pivotRow["date"] . "," . implode(",", $pivotRow["months"]) . "," . implode(",", $pivotRow["quarters"]) . ")";
85  }
86 
87  $this->cte= "with monthPivotCTE(yearmo,m0,m1,m2,m3,m4,m5,m6,m7,m8,m9,m10,m11,m12,q0,q1,q2,q3) as (values " . implode(",", $cteArray) . ")";
88  return $this->cte;
89  }
90 
91  /**
92  * public function getColumnSQL($toBeSummed)
93  *
94  * This function gets the SQL for the columns. Each column looks like "sum($toBeSummed * monthColumn)".
95  *
96  * @param string $toBeSummed -- This is what needs to be summed to be summed by month.
97  * @return string -- Add this to the column section of the SQL.
98  */
99  public function getColumnSQL($toBeSummed)
100  {
101  if (isset($this->columnSQLArray[$toBeSummed]))
102  return $this->columnSQLArray[$toBeSummed];
103 
104  $columnArray= array();
105  for($i=0; $i <= 12; $i++)
106  {
107  $columnArray[]= "sum($toBeSummed * m$i)";
108  }
109 
110  $this->columnSQLArray[$toBeSummed]= implode(",", $columnArray);
111  return $this->columnSQLArray[$toBeSummed];
112  }
113 
114  /**
115  * public function getColumns($columnArray)
116  *
117  * This function gets the column list.
118  *
119  * @param array $columnArray -- Set the column list as usual here minus the columns for the pivot.
120  * @return array $columnArray -- The starting columns plus "MTD" => "number", "Jan" => "number", "Dec" => "number" etc. (That would be if the current date was in February.)
121  */
122  public function getColumns($columnArray)
123  {
124  $first= true;
125  foreach($this->pivotArray as $pivotRow)
126  {
127  $col= $first ? "MTD" : $pivotRow["title"];
128  $columnArray[$col]= "number";
129  $first= false;
130  }
131  return $columnArray;
132  }
133 }
134 
135 /**
136  * function getIsFilterChanged()
137  * This function determines if the filter is changed. If it is, then we will also need the count.
138  *
139  * Javascript parameters:
140  * @param Array oldFilter -- the previous filter
141  * @param Array newFilter -- the new filter
142  */
143 function getIsFilterChanged() { ?>
144 function isFilterChanged(oldFilter, newFilter)
145 {
146  // Take care of nulls
147  if (oldFilter == null)
148  {
149  if (newFilter == null)
150  return false;
151  else return true;
152  }
153  else if (newFilter == null)
154  return true;
155 
156  var oldConstructor= oldFilter.constructor;
157  var newConstructor= newFilter.constructor;
158  var oldLogic, oldFilters, newLogic, newFilters;
159  if (oldConstructor === Array)
160  {
161  oldLogic= "and";
162  oldFilters= oldFilter;
163  }
164  else if (oldConstructor === Object)
165  {
166  oldLogic= oldFilter.logic == null ? "and" : oldFilter.logic.trim();
167  oldFilters= oldFilter.filters;
168  }
169  else return false; // Invalid so in theory the filter wont change.
170 
171  if (newConstructor === Array)
172  {
173  newLogic= "and";
174  newFilters= newFilter;
175  }
176  else if (newConstructor === Object)
177  {
178  newLogic= newFilter.logic == null ? "and" : newFilter.logic.trim();
179  newFilters= newFilter.filters;
180  }
181  else return false; // Invalid so in theory the filter wont change.
182 
183  if (oldLogic != newLogic)
184  return true;
185  var oldFiltersEmpty= oldFilters == null || oldFilters.constructor !== Array || oldFilters.length == 0;
186  var newFiltersEmpty= newFilters == null || newFilters.constructor !== Array || newFilters.length == 0;
187  if (oldFiltersEmpty != newFiltersEmpty)
188  return true;
189  if (oldFilters.length != newFilters.length)
190  return true;
191 
192  var map= {};
193  for(var i=0; i!= oldFilters.length; i++)
194  {
195  var record= oldFilters[i];
196  if (record.field == null || record.operator == null || record.value == null)
197  return false; // Invalid so in theory the filter won't change
198  map[""+record.field+"||"+record.operator+"||"+record.value]= true;
199  }
200  for(var i=0; i!= newFilters.length; i++)
201  {
202  var record= newFilters[i];
203  if (record.field == null || record.operator == null || record.value == null)
204  return false; // Invalid so in theory the filter won't change
205  if (map[""+record.field+"||"+record.operator+"||"+record.value] == null)
206  return true;
207  }
208  return false;
209 }
210 <?php }
211 
212 /**
213  * function initReport($reportPage, $report, $pageable="true", $pageSize=30, $printerFriendly=false, $sort="", $filter="")
214  * Prints out the init function to create the report grid with the appropriate events. This is server-side. Please refer to the next function for client-side.
215  *
216  * PHP parameters:
217  * @param string $reportPage -- the url of the report page for calling the database functions readReport and readCSV.
218  * @param string $report -- the report id in the report global object for getting the SQL and columns.
219  * @param string $pageable -- "true" or "false." Refers to the kendo attribute pageable.
220  * @param integer $pageSize -- what the size of the page is.
221  * @param boolean $printerFriendly -- if true, then all the controls do not show up on the page.
222  * @param string $sort -- a json_encoded array of the initial sort.
223  * @param string $filter -- a json_encoded array of the initial filter.
224  *
225  * Javascript parameters:
226  * @param Function afterInitFunc -- if defined, then this function will be called after the report grid is initialized.
227  * @param Function parameterMapFunc -- if defined, then this function will be called before the read operation. It can be used to add a parameter.
228  * @param Function parseFunc -- if defined, then this function will be called after the read operation.
229  * @param Function dataBoundFunc -- if defined, then this function will be called on the dataBound.
230  * @param Function preInitFunc -- if defined, then this function will be called after model and column data received but before initialization
231  */
232 function initReport($reportPage, $report, $pageable="true", $pageSize=30, $printerFriendly=false, $sort="", $filter="") {
233  $dataUrl= $reportPage . (false !== strpos($reportPage, "?") ? "&" : "?") . "operation=readReport"; // Ensure that if there is already a query string to append to it.
234 ?>
235 var filterChanged= true;
236 function initReport(afterInitFunc, parameterMapFunc, parseFunc, dataBoundFunc, preInitFunc)
237 {
238  var initial= true;
239  var pageSize= <?php echo $pageSize; ?>;
240  var count= 0;
241  var currentFilter= null;
242  showWaitWindow();
243  $.post("<?php echo $dataUrl; ?>", {report: "<?php echo $report;?>", limit: pageSize, includeSchema: "Y", excludeData: "Y"},
244  function(topData) {
245  hideWaitWindow();
246  showSQL(topData);
247  if (!errorsAreShown(topData, "formValidateMainDiv"))
248  {
249  if (typeof(preInitFunc) == "function")
250  (preInitFunc)(topData);
251  count= topData.reportData.total;
252  var sort= <?php echo trim($sort) == "" ? '{field: topData.columnData[0].field, dir: "asc"};' : "$sort;"; ?>
253  var filter= <?php echo trim($filter) == "" ? "[];" : "$filter;"; ?>
254  var grid= $("#reportGrid").kendoGrid({
255  dataSource: {
256  transport: {
257  read: {
258  url: "<?php echo $dataUrl; ?>",
259  dataType: "json",
260  type: "POST"
261  },
262  parameterMap: function(data, type) {
263  <?php if ($printerFriendly) { ?>
264  var parameters= {report: "<?php echo $report; ?>", limit: -1, offset: 0};
265  <?php } else { ?>
266  var parameters= {report: "<?php echo $report; ?>", limit: pageSize, offset: data.skip};
267  <?php } ?>
268  if (data.sort != null && data.sort.length > 0)
269  {
270  var sortCols= [], sortDirs= [];
271  for(var i=0, length=data.sort.length; i!= length; i++)
272  {
273  var record= data.sort[i];
274  sortCols.push(record.field);
275  sortDirs.push(record.dir);
276  }
277  parameters.sortCols= kendo.stringify(sortCols);
278  parameters.sortDirs= kendo.stringify(sortDirs);
279  }
280  if (data.filter != null)
281  {
282  var filterCols= [], filterOps= [], filterValues= [];
283  var filters= data.filter.filters;
284  for(var i=0, length=filters.length; i!= length; i++)
285  {
286  var record= filters[i];
287  filterCols.push(record.field);
288  filterOps.push(record.operator);
289 
290  if (record.value.constructor == Date)
291  filterValues.push(kendo.toString(record.value, 'yyyy-MM-dd'));
292  else
293  filterValues.push(record.value);
294  }
295  parameters.filterCols= kendo.stringify(filterCols);
296  parameters.filterOps= kendo.stringify(filterOps);
297  parameters.filterValues= kendo.stringify(filterValues);
298  }
299  filterChanged= initial || isFilterChanged(currentFilter, data.filter);
300  currentFilter= data.filter;
301 
302  if (typeof(parameterMapFunc) == "function")
303  (parameterMapFunc)(data, parameters, filterChanged);
304 
305  if (filterChanged)
306  {
307  parameters.includeCount= "Y";
308  filterChanged= false;
309  }
310  initial= false;
311  return parameters;
312  }
313  },
314  schema: {
315  model: topData.modelData,
316  data: "data",
317  total: "total",
318  parse: function(data) {
319  showSQL(data);
320  if (!errorsAreShown(data, "formValidateMainDiv"))
321  {
322  var countChanged= false;
323  if (data.reportData.total == -1)
324  {
325  data.reportData.total= count;
326  }
327  else
328  {
329  count= data.reportData.total;
330  countChanged= true;
331  }
332 
333  if (typeof(parseFunc) == "function")
334  (parseFunc)(data, countChanged);
335  }
336  return data.reportData;
337  }
338  },
339  serverSorting: true,
340  serverPaging: true,
341  serverFiltering: true,
342  pageSize: pageSize,
343  sort: sort,
344  filter: filter
345  },
346  columns: topData.columnData,
347  <?php if (!$printerFriendly) { ?>
348  pageable: <?php echo $pageable; ?>,
349  sortable: true,
350  filterable: {
351  extra: false
352  },
353  dataBound: function(e) {
354  if (typeof(dataBoundFunc) == "function")
355  (dataBoundFunc) (this, e);
356  },
357  <?php } ?>
358  scrollable: false
359  }).data("kendoGrid");
360 
361  <?php if (!$printerFriendly) { ?>
362  $("#downloadCSVBtn").click(function () {
363  $("#downloadCSVForm .extraCSVForm").remove();
364  var sort= grid.dataSource.sort();
365  if (sort != null)
366  {
367  var sortCols= [], sortDirs= [];
368  for(var i=0, length=sort.length; i!= length; i++)
369  {
370  var record= sort[i];
371  sortCols.push(record.field);
372  sortDirs.push(record.dir);
373  }
374  $("#downloadCSVForm").append("<input class='extraCSVForm' name='sortCols' value='" + kendo.stringify(sortCols) + "'>");
375  $("#downloadCSVForm").append("<input class='extraCSVForm' name='sortDirs' value='" + kendo.stringify(sortDirs) + "'>");
376  }
377  var filter= grid.dataSource.filter();
378  if (filter != null)
379  {
380  var filterCols= [], filterOps= [], filterValues= [];
381  var filters= filter.filters;
382  for(var i=0, length=filters.length; i!= length; i++)
383  {
384  var record= filters[i];
385  filterCols.push(record.field);
386  filterOps.push(record.operator);
387  filterValues.push(record.value);
388  }
389  $("#downloadCSVForm").append("<input class='extraCSVForm' name='filterCols' value='" + kendo.stringify(filterCols) + "'>");
390  $("#downloadCSVForm").append("<input class='extraCSVForm' name='filterOps' value='" + kendo.stringify(filterOps) + "'>");
391  $("#downloadCSVForm").append("<input class='extraCSVForm' name='filterValues' value='" + kendo.stringify(filterValues) + "'>");
392  }
393  $("#downloadCSVForm").submit();
394  });
395 
396  if (typeof(afterInitFunc) == "function")
397  afterInitFunc();
398  <?php } ?>
399  }
400  });
401 }
402 <?php }
403 
404 /**
405  * function initReportClientSide($reportPage, $report, $title, $additionalParameters= array(), $scrollable=false, $beforeInitFunc="", $afterInitFunc="")
406  * Prints out the init function to create the report grid with the appropriate events. This is client-side. Please refer to the previous function for server-side.
407  *
408  * PHP parameters:
409  * @param string $reportPage -- the url of the report page for calling the database functions readReport and readCSV.
410  * @param string $report -- the report id in the report global object for getting the SQL and columns.
411  * @param string $title -- the name of the CSV download.
412  * @param array $additionalParameters -- parameters to be added to the default parameters to get all the data.
413  * @param boolean $scrollable -- refers to the kendo scrollable property
414  * @param string $beforeInitFunc -- Javascript function call before the report grid is initialized.
415  * @param string $afterInitFunc -- Javascript function call after the report grid is initialized.
416  */
417 function initReportClientSide($reportPage, $report, $title, $additionalParameters= array(), $scrollable=false, $beforeInitFunc="", $afterInitFunc="") {
418  $dataUrl= $reportPage . (false !== strpos($reportPage, "?") ? "&" : "?") . "operation=readReport"; // Ensure that if there is already a query string to append to it.
419  $parameters= array("report" => $report, "limit" => -1, "offset" => 0, "includeSchema" => "Y");
420  foreach($additionalParameters as $key => $value)
421  {
422  $parameters[$key]= $value;
423  }
424 ?>
425 function initReport()
426 {
427  var initial= true;
428  var pageSize= 30;
429  var count= 0;
430  var currentFilter= null;
431  showWaitWindow();
432  $.post("<?php echo $dataUrl; ?>", <?php echo json_encode($parameters); ?>,
433  function(topData) {
434  hideWaitWindow();
435  showSQL(topData);
436  if (!errorsAreShown(topData, "formValidateMainDiv"))
437  {
438  var sort= {field: topData.columnData[0].field, dir: "asc"};
439  var gridDefinition= {
440  dataSource: {
441  transport: {
442  read: function (options) {
443  options.success(topData.reportData.data);
444  }
445  },
446  schema: {
447  model: topData.modelData
448  },
449  pageSize: pageSize,
450  sort: sort
451  },
452  columns: topData.columnData,
453  pageable: true,
454  sortable: true,
455  filterable: {
456  extra: false
457  },
458  scrollable: <?php echo ($scrollable ? "true" : "false"); ?>
459  };
460 
461  <?php echo ($beforeInitFunc != "" ? "($beforeInitFunc) (gridDefinition);" : ""); ?>
462 
463  var grid= $("#reportGrid").kendoGrid(gridDefinition).data("kendoGrid");
464 
465  $("#downloadCSVBtn").click(function () {
466  downloadCSVClientSide();
467  });
468 
469  <?php echo ($afterInitFunc != "" ? "($afterInitFunc) (grid);" : ""); ?>
470  }
471  });
472 }
473 
474 <?php
475 /**
476  * function downloadCSVClientSide()
477  *
478  * Creates the CSV from what is in the grid. Filter and sort are preserved. Includes all the pages and then when done, sets the page back to the current page.
479  * {@link http://stackoverflow.com/questions/14964035/how-to-export-javascript-array-info-to-csv-on-client-side}
480  */
481 ?>
482 function downloadCSVClientSide()
483 {
484  var grid= $("#reportGrid").data("kendoGrid");
485  var csvData= [];
486  var csvRecord= [];
487  var columns= [];
488  for(var i=0, length= grid.columns.length; i!= length; i++)
489  {
490  var record= grid.columns[i];
491 
492  if (record.hidden == null || !record.hidden)
493  {
494  columns.push(record.field);
495  csvRecord.push(csvEscape(record.title));
496  }
497  }
498  csvData.push(csvRecord.join());
499 
500  var data= grid.dataSource.data();
501  var filter= grid.dataSource.filter();
502  var sort= grid.dataSource.sort();
503  var query = new kendo.data.Query(data);
504  data= query.filter(filter).sort(sort).data;
505 
506  for(var i=0; i!= data.length; i++)
507  {
508  var record= data[i];
509  csvRecord= [];
510 
511  for(var j=0; j!= grid.columns.length; j++)
512  {
513  if (record.hidden == null || !record.hidden)
514  {
515  var column= grid.columns[j];
516  var kendoCol= column.field;
517  var value= column.template != null ? kendo.template(record[kendoCol]) : (column.format != null ? kendo.format(column.format, record[kendoCol]) : record[kendoCol]);
518  csvRecord.push(csvEscape(value));
519  }
520  }
521 
522  csvData.push(csvRecord.join());
523  }
524 
525  var blob = new Blob([csvData.join("\r\n")], { type: 'text/csv;charset=utf-8;' });
526  var filename= "<?php echo $title; ?>.csv";
527  if (navigator.msSaveBlob) // IE 10+
528  navigator.msSaveBlob(blob, filename);
529  else
530  {
531  var link = document.createElement("a");
532  if (link.download != null)
533  { // feature detection
534  // Browsers that support HTML5 download attribute
535  var url = URL.createObjectURL(blob);
536  link.setAttribute("href", url);
537  link.setAttribute("download", filename);
538  link.style.visibility = 'hidden';
539  document.body.appendChild(link);
540  link.click();
541  document.body.removeChild(link);
542  }
543  }
544 }
545 
546 <?php
547 /**
548  * function csvEscape(string)
549  * Escapes the string for CSV. That means that if there is a newline or a double quote, it needs to be escaped.
550  *
551  * @param string $string -- the string to convert.
552  * @return string -- the converted string.
553  */
554 ?>
555 function csvEscape(string)
556 {
557  if (string == null || typeof(string) != "string")
558  return string;
559  string= string.indexOf('"') != -1 ? string.replace('"', '""') : string;
560  string= string.indexOf(',') != -1 || string.indexOf('\n') != -1 ? '"' + string + '"' : string;
561  return string;
562 }
563 <?php } // end initReportClientSide
564 
565 /**
566  * function getReportTitles()
567  *
568  * Get the titles of the reports.
569  * @return array -- titles of the reports.
570  */
571 function getReportTitles()
572 {
573  global $reportList;
574  $reportTitles= array();
575  foreach($reportList as $key => $record)
576  {
577  $reportTitles[$key]= isset($record["title"]) ? $record["title"] : $key;
578  }
579  return $reportTitles;
580 }
581 
582 /**
583  * function getCamelCase($string)
584  * Removes the whitespaces and capitalizes the first letter in each word for keys.
585  *
586  * @param string $string -- the string to convert.
587  * @return string -- the converted string.
588  */
589 function getCamelCase($string)
590 {
591  $camelCase= lcfirst(str_replace(" ", "", ucwords(strtolower(trim(preg_replace("/[^a-z0-9 ]+/i", " ", $string)))))); // the column is camelcase with only letters and numbers.
592  return is_numeric($camelCase[0]) ? "n$camelCase" : $camelCase; // Kendo seems to complain if the column starts with a number.
593 }
594 
595 /**
596  * function compileMenu($conf, $reportList, $reportFrontEndFile, $useRadioButtons=false)
597  * Creates the menu for all the reports.
598  *
599  * @param array $conf -- the page layout configuration.
600  * @param array $reportList -- the report configuration.
601  * @param string $reportFrontEndFile -- the URL.
602  * @param boolean $useRadioButtons -- If true, then to select, you would click on a radio button. Otherwise, the list is shown with links.
603  *
604  */
605 function compileMenu($conf, $reportList, $reportFrontEndFile, $useRadioButtons=false)
606 {
607  $menu= "<table class='reportMenu'>";
608  foreach($conf as $sectionTitle => $reports)
609  {
610  $menu.= "<tr class='sectionHeader'><td>$sectionTitle</td></tr>";
611  $count=1;
612  foreach($reports as $report)
613  {
614  $class= $count % 2 == 1 ? "odd" : "even";
615  $reportTitle= isset($reportList[$report]["title"]) ? $reportList[$report]["title"] : $report;
616  $href= isset($reportList[$report]["url"]) ? $reportList[$report]["url"] : $reportFrontEndFile . (false !== strpos($reportFrontEndFile, "?") ? "&" : "?") . "report=$report";
617  // Make sure that there no two ? in the query string
618  $menu.= "<tr class='$class'><td>" . ($useRadioButtons ? "<input type='radio' class='reportRadioButton' name='reportRadioButton' value='$href'> $reportTitle" : "<a href=$href>$reportTitle</a>");
619  $menu.= "</td></tr>";
620  $count++;
621  }
622  }
623  $menu.= "</table>";
624  return $menu;
625 }
626 
627 /**
628  * function getSchemaUsingBaseSQL($dbh, &$baseSQL, $dateFormat, $dateIfNull, $colArray, $replaceString, $replaceCode, $values, &$sqls, &$errors, &$modelData, &$columnData, $includeSchema,
629  * $report, $defaultDateFormat, $cookieName="")
630  * This gets the schema using the base sql.
631  *
632  * @param integer $dbh -- the database connection
633  * @param string $baseSQL -- the SQL from the report configuration. This will be expanded with the sort and filtering clauses.
634  * @param string $dateFormat -- the format for the date.
635  * @param string $dateIfNull -- text to display if a date is null.
636  * @param array $colArray -- array of the columns in the report.
637  * @param string $replaceString -- string to replace code with.
638  * @param string $replaceCode -- string to replace.
639  * @param array $values -- list of DDLs that a column might have.
640  * @param array $sqls -- list of all SQLs.
641  * @param array $errors -- list of all errors.
642  * @param array $modelData -- will push to this array for the model attribute in the kendo grid.
643  * @param array $columnData -- will push to this array for the column attribute in the kendo grid.
644  * @param boolean $includeSchema -- if true, something doesn't happen.
645  * @param string $report -- the name of the report.
646  * @param string $defaultDateFormat -- the default date format.
647  * @param string $cookieName -- what the cookie name is. This is only relevant if the report has replaceString and replaceCode set. If this is set, then whatever function to get replaceString
648  * is called one time and put in a cookie.
649  */
650 function getSchemaUsingBaseSQL($dbh, &$baseSQL, $dateFormat, $dateIfNull, $colArray, $replaceString, $replaceCode, $values, &$sqls, &$errors, &$modelData, &$columnData, $includeSchema, $report,
651  $defaultDateFormat, $cookieName="")
652 {
653  $cols= array();
654 
655  foreach($colArray as $colTitle => $colType)
656  {
657  $kendoCol= getCamelCase($colTitle);
658  $modelRecord= array("type" => $colType == "ignore" ? "string" : $colType);
659  $newTitle= explode(".", $colTitle);
660  $newTitle= $newTitle[0];
661  $columnRecord= array("field" => $kendoCol, "title" => $newTitle, "type" => $colType);
662  if ($colType == "date")
663  {
664  $ifNull= isset($dateIfNull) ? trim($dateIfNull) : "";
665  $format= isset($dateFormat) ? trim($dateFormat) : (isset($defaultDateFormat) ? trim($defaultDateFormat) : "");
666  if ($format != "")
667  $columnRecord["template"]= "# if ($kendoCol == null || $kendoCol == '') { # $ifNull # } else { # #: kendo.toString(new Date($kendoCol), '$format') # # } #";
668  }
669  else if ($colType == "number")
670  {
671  $columnRecord["attributes"]= array("style" => "text-align: right;");
672  $columnRecord["headerAttributes"]= array("style" => "text-align: right;");
673  }
674  else if ($colType == "currency")
675  {
676  $columnRecord["type"]= "number";
677  $modelRecord["type"]= "number";
678  $columnRecord["attributes"]= array("style" => "text-align: right;");
679  $columnRecord["headerAttributes"]= array("style" => "text-align: right;");
680  $columnRecord["format"]= "{0:c2}";
681  }
682  else if ($colType == "list")
683  {
684  $columnRecord["type"]= "string";
685  $modelRecord["type"]= "string";
686 
687  $ddl= array();
688  foreach($values[$colTitle] as $value => $text)
689  {
690  $ddl[]= array("text" => $text, "value" => $value);
691  }
692  $columnRecord["values"]= $ddl;
693  }
694 
695  $modelData[$kendoCol]= $modelRecord;
696 
697  if ($colType != "ignore")
698  $columnData[]= $columnRecord;
699  $cols[]= $kendoCol;
700  }
701 
702  if (isset($replaceString) && isset($replaceCode))
703  {
704  $cookie= array();
705  $replaceCodeString= "";
706  $cookieExists= false;
707  if (isset($_COOKIE[$cookieName]))
708  {
709  parse_str($_COOKIE[$cookieName], $cookie);
710  $cookieExists= true;
711  }
712 
713  if (!$includeSchema && $cookieExists && isset($cookie[$report]) && trim($cookie[$report]) != "")
714  {
715  $replaceCodeString= trim($cookie[$report]);
716  }
717  else
718  {
719  $replaceCodeString= eval($replaceCode);
720  $cookie[$report]= $replaceCodeString;
721  $expiresInDay= time()+86400;
722 
723  setcookie($cookieName, http_build_query($cookie), $expiresInDay, "/", $_SERVER['SERVER_NAME']);
724  }
725  $baseSQL= str_replace($replaceString, $replaceCodeString, $baseSQL);
726  }
727 
728  updateSelectSQL($baseSQL, $cols, $errors);
729 }
730 
731 /**
732  * function updateSelectSQL(&$baseSQL, $cols, &$errors)
733  * updates the select SQL with the keys based on the camelcase of the titles of the columns.
734  *
735  * @param string $baseSQL -- SQL to update.
736  * @param array $cols -- the columns in the global report array.
737  * @param array $errors -- the errors.
738  */
739 function updateSelectSQL(&$baseSQL, $cols, &$errors)
740 {
741  $stack=array();
742  $word= "";
743  $nonAlphaFound= false;
744  $selectFound= false;
745  $asFound= false;
746  $inExpression= true;
747  $fromIndex= -1;
748  $selectArray= "";
749  $selectCalc= "";
750  $selectName= "";
751  $selectIndex= 0;
752  $whereFound= false;
753  for($i=0,$count=strlen($baseSQL); $i != $count; $i++)
754  {
755  $char= $baseSQL[$i];
756 
757  if(ctype_alnum ($char) || $char == "_")
758  {
759  $word.= $char;
760  $nonAlphaFound= false;
761  }
762  else
763  {
764  $willNotBeInExpression= false;
765  $numInStack= count($stack);
766  $stackEmpty= $numInStack == 0;
767  $lastInStack= $stackEmpty ? "" : $stack[$numInStack-1];
768 
769  if ($selectFound && $word != "" && !$nonAlphaFound && $stackEmpty)
770  $inExpression= false;
771 
772  switch(strtolower($word))
773  {
774  case "select":
775  if (!$selectFound && $stackEmpty)
776  $selectFound= true;
777  break;
778  case "as":
779  if ($selectFound && $stackEmpty)
780  $asFound= true;
781  break;
782  case "from":
783  if ($selectFound && $stackEmpty)
784  {
785  $selectArray.= "$selectCalc as " . $cols[$selectIndex++];
786  $fromIndex= $i;
787  }
788  break;
789  case "case":
790  if (!$nonAlphaFound)
791  {
792  array_push($stack, $word);
793  $inExpression= true;
794  }
795  break;
796  case "end":
797  if ($lastInStack == "case" && !$nonAlphaFound)
798  {
799  array_pop($stack);
800  if (count($stack) == 0)
801  {
802  $willNotBeInExpression= true;
803  $stackEmpty= true;
804  }
805  else // Need to update the lastInStack variable for the case of "end)"
806  $lastInStack= $stack[$numInStack-2];
807  }
808  break;
809  case "distinct":
810  $inExpression= true;
811  break;
812  }
813 
814  $nonAlphaFound= true;
815  if ($fromIndex != -1)
816  break;
817  else if ($word != "")
818  $selectCalc.= $word;
819 
820  switch ($char)
821  {
822  case "(":
823  case "[":
824  case "{":
825  array_push($stack, $char);
826  $inExpression= true;
827  break;
828  case "'":
829  case '"':
830  if ($lastInStack == $char)
831  {
832  array_pop($stack);
833  if (count($stack) == 0)
834  $willNotBeInExpression= true;
835  }
836  else
837  {
838  array_push($stack, $char);
839  }
840 
841  break;
842  case "}":
843  if ($lastInStack == "{")
844  {
845  array_pop($stack);
846  if (count($stack) == 0)
847  $willNotBeInExpression= true;
848  }
849  break;
850  case "]":
851  if ($lastInStack == "[")
852  {
853  array_pop($stack);
854  if (count($stack) == 0)
855  $willNotBeInExpression= true;
856  }
857  break;
858  case ")":
859  if ($lastInStack == "(")
860  {
861  array_pop($stack);
862  if (count($stack) == 0)
863  $willNotBeInExpression= true;
864  }
865 
866  break;
867  case ",":
868  if ($selectFound && $stackEmpty)
869  {
870  $selectArray.= "$selectCalc as " . $cols[$selectIndex++];
871  $selectCalc= "";
872  $selectName= "";
873  $asFound= false;
874  $inExpression= true;
875  }
876  break;
877  case "|":
878  case "+":
879  case "/":
880  case "%":
881  case "-":
882  case "<":
883  case ">":
884  case ".":
885  case "*":
886  case "&":
887  case "^":
888  case "~":
889  case "`":
890  case ":":
891  case "=":
892  if ($stackEmpty)
893  $inExpression= true;
894  break;
895  }
896  $word= "";
897  if ($inExpression)
898  $selectCalc.= $char;
899  $inExpression= !$willNotBeInExpression;
900  }
901 
902  }
903 
904  $restOfSQL= substr($baseSQL, $fromIndex);
905  $baseSQL= "$selectArray from $restOfSQL";
906 }
907 
908 /**
909  * function getData($dbh, $fullSQL, &$reportData, &$modelData, &$columnData, &$sqls, &$errors, $dontIncludeId=false, $excludeData= false)
910  * Retrieves the data for the report from the database.
911  *
912  * @param integer $dbh -- the database connection
913  * @param string $fullSQL -- the full SQL (with the clauses for sorting and filtering)
914  * @param array $reportData -- the array that contains the data to send to kendo.
915  * @param array $modelData -- the array that contains the model data to send to kendo.
916  * @param array $columnData -- the array that contains the column data to send to kendo.
917  * @param array $sqls -- the SQLs.
918  * @param array $errors -- the errors.
919  * @param boolean $dontIncludeId -- for CSV. If it is not CSV, then there is a special id for kendo.
920  * @param boolean $excludeData -- if true, then it will do anything else except for actually retrieving the data.
921  */
922 function getData($dbh, $fullSQL, &$reportData, &$modelData, &$columnData, &$sqls, &$errors, $dontIncludeId=false, $excludeData= false)
923 {
924  $colMap= array();
925  foreach($modelData as $key => $unused)
926  {
927  $colMap[strtolower($key)]= $key;
928  }
929 
930  // Add id so Kendo knows what to do.
931  if (!$dontIncludeId)
932  {
933  $idName= "reportId";
934  $modelData[$idName]= array("type" => "number");
935  $modelData= array("id" => $idName, "fields" => $modelData);
936  }
937  else
938  {
939  $modelData= array("fields" => $modelData);
940  }
941 
942  if (!$excludeData)
943  {
944  $sqls[]= $fullSQL;
945  $sth = db_query($fullSQL,$dbh);
946  $newError= trim(db_last_error());
947  if ($newError != "")
948  {
949  $errors[]= $newError;
950  db_free_result($sth);
951  }
952 
953  $booleanFields= array();
954  $index= 0;
955  foreach($modelData["fields"] as $kendoCol => $kendoColArray)
956  {
957  if ($kendoColArray["type"] == "boolean")
958  $booleanFields[$kendoCol]= array("trueFound" => false, "falseFound" => false, "trueValue" => "", "falseValue" => "", "index" => $index);
959  $index++;
960  }
961 
962  $i= 0;
963  while($dRecord = db_fetch_assoc($sth, $i++)) // This will keep the camelcase. The database puts everything as lowercase.
964  {
965  $kendoRecord= array();
966  if (!$dontIncludeId)
967  $kendoRecord[$idName]= $i;
968 
969  foreach($dRecord as $key => $value)
970  {
971  $kendoCol= $colMap[strtolower($key)];
972  $value= trim($value);
973 
974  if (isset($booleanFields[$kendoCol]))
975  {
976  $falseFound= $booleanFields[$kendoCol]["falseFound"];
977  $trueFound= $booleanFields[$kendoCol]["trueFound"];
978  $updateTrue= false;
979  $updateFalse= false;
980  if (!$trueFound || !$falseFound)
981  {
982  switch($value)
983  {
984  case "Yes":
985  if (!$falseFound)
986  $booleanFields[$kendoCol]["falseValue"]= "No";
987  $updateTrue= true;
988  break;
989  case "yes":
990  if (!$falseFound)
991  $booleanFields[$kendoCol]["falseValue"]= "no";
992  $updateTrue= true;
993  break;
994  case "Y":
995  if (!$falseFound)
996  $booleanFields[$kendoCol]["falseValue"]= "N";
997  $updateTrue= true;
998  break;
999  case "y":
1000  if (!$falseFound)
1001  $booleanFields[$kendoCol]["falseValue"]= "n";
1002  $updateTrue= true;
1003  break;
1004  case "1":
1005  if (!$falseFound)
1006  $booleanFields[$kendoCol]["falseValue"]= "0";
1007  $updateTrue= true;
1008  break;
1009  case "true":
1010  if (!$falseFound)
1011  $booleanFields[$kendoCol]["falseValue"]= "false";
1012  $updateTrue= true;
1013  break;
1014  case "True":
1015  if (!$falseFound)
1016  $booleanFields[$kendoCol]["falseValue"]= "False";
1017  $updateTrue= true;
1018  break;
1019  case "No":
1020  if (!$trueFound)
1021  $booleanFields[$kendoCol]["trueValue"]= "Yes";
1022  $updateFalse= true;
1023  break;
1024  case "no":
1025  if (!$trueFound)
1026  $booleanFields[$kendoCol]["trueValue"]= "yes";
1027  $updateFalse= true;
1028  break;
1029  case "n":
1030  if (!$trueFound)
1031  $booleanFields[$kendoCol]["trueValue"]= "y";
1032  $updateFalse= true;
1033  break;
1034  case "N":
1035  if (!$trueFound)
1036  $booleanFields[$kendoCol]["trueValue"]= "Y";
1037  $updateFalse= true;
1038  break;
1039  case "0":
1040  if (!$trueFound)
1041  $booleanFields[$kendoCol]["trueValue"]= "1";
1042  $updateFalse= true;
1043  break;
1044  case "false":
1045  if (!$trueFound)
1046  $booleanFields[$kendoCol]["trueValue"]= "true";
1047  $updateFalse= true;
1048  break;
1049  case "False":
1050  if (!$trueFound)
1051  $booleanFields[$kendoCol]["trueValue"]= "True";
1052  $updateFalse= true;
1053  break;
1054  default:
1055  $updateFalse= true;
1056  }
1057 
1058  if ($updateTrue)
1059  {
1060  $booleanFields[$kendoCol]["trueValue"]= $value;
1061  $booleanFields[$kendoCol]["trueFound"]= true;
1062  $kendoRecord[$kendoCol]= true;
1063  }
1064  else if ($updateFalse)
1065  {
1066  $booleanFields[$kendoCol]["falseValue"]= $value;
1067  $booleanFields[$kendoCol]["falseFound"]= true;
1068  $kendoRecord[$kendoCol]= false;
1069  }
1070  }
1071  }
1072  else
1073  {
1074  $kendoRecord[$kendoCol]= trim($value);
1075  }
1076  }
1077  $reportData[]= $kendoRecord;
1078  }
1079  db_free_result($sth);
1080 
1081  foreach($booleanFields as $kendoCol => $record)
1082  {
1083  $columnData[$record["index"]]["values"]= array(array("text" => $record["trueValue"], "value" => true), array("text" => $record["falseValue"], "value" => false));
1084  }
1085  }
1086 }
1087 
1088 /**
1089  * function addSort(&$sql, &$errors, $sortCols, $sortDirs, $modelData)
1090  * Add the sort clause to the SQL.
1091  *
1092  * @param array $sql -- the SQLs.
1093  * @param array $errors -- the errors.
1094  * @param string $sortCols -- the columns to sort.
1095  * @param string $sortDirs -- the directions of the columns to sort.
1096  * @param array $modelData -- the model data. This is included because if there isn't any sort, then the first column in the model is sorted.
1097  */
1098 function addSort(&$sql, &$errors, $sortCols, $sortDirs, $modelData)
1099 {
1100  $sortCols= trim($sortCols);
1101  $sortDirs= trim($sortDirs);
1102 
1103  if ($sortCols != "" && $sortDirs != "")
1104  {
1105  $colsExpanded= json_decode($sortCols, true);
1106  $dirsExpanded= json_decode($sortDirs, true);
1107  if (!is_array($colsExpanded))
1108  $errors[]= "Sort columns is not a valid array.";
1109  if (!is_array($dirsExpanded))
1110  $errors[]= "Sort directions is not a valid array.";
1111  if (count($colsExpanded) != count($dirsExpanded))
1112  $errors[]= "Sort arrays need to be the same size.";
1113  $sortSQL= array();
1114  $index= 0;
1115  $allValidDirs= true;
1116  foreach($colsExpanded as $col)
1117  {
1118  $type= trim($modelData[$col]["type"]);
1119  $dir= strtolower(strval($dirsExpanded[$index]));
1120  $databaseCol= strtolower($type) == "string" ? "nullif(trim($col), '')" : $col;
1121  if (in_array($dir, array("asc", "desc")) === false)
1122  $allValidDirs= false;
1123  $sortSQL[]= "$databaseCol $dir";
1124  if (!$allValidDirs)
1125  break;
1126  $index++;
1127  }
1128  if (!$allValidDirs)
1129  $errors[]= "Sort directions must be either asc or desc.";
1130 
1131  if (count($sortSQL) != 0)
1132  $sql.= " order by " . implode(", ", $sortSQL);
1133  }
1134  else if (($sortCols != "" && $sortDirs == "") || ($sortCols == "" && $sortDirs != ""))
1135  $errors[]= "Both sort column and sort direction arrays must be defined for a sort.";
1136  else // There is always a sort. If there isn't one, then it is the first column asc
1137  {
1138  $first= true;
1139  foreach($modelData as $kendoCol => $kendoColArray)
1140  {
1141  if ($first)
1142  {
1143  $sql.= " order by " . (strtolower($kendoColArray["type"]) == "string" ? "trim(coalesce($kendoCol, ''))" : $kendoCol) . " asc";
1144  $first= false;
1145  }
1146  }
1147  }
1148 }
1149 
1150 /**
1151  * function cmpSort($a, $b, $compArray)
1152  * This is a custom sort function for if the sort needed to happen after it was retrieved from the database. In the case of getting data from www3, www5, www6, conbining and then sorting.
1153  *
1154  * @param string $a -- the first object to check
1155  * @param string $b -- the second object to check
1156  * @param array $compArray -- equivalent to the sort array in kendo.
1157  */
1158 function cmpSort($a, $b, $compArray)
1159 {
1160  foreach($compArray as $comp)
1161  {
1162  switch($comp["type"])
1163  {
1164  case "string":
1165  $aComp= trim($a[$comp["field"]]);
1166  $bComp= trim($b[$comp["field"]]);
1167  $comp= $comp["dir"] == "asc" ? strcmp($aComp, $bComp) : strcmp($bComp, $aComp);
1168  break;
1169  case "number":
1170  $aComp= floatval($a[$comp["field"]]);
1171  $bComp= floatval($b[$comp["field"]]);
1172  $comp= $comp["dir"] == "asc" ? $aComp - $bComp : $bComp - $aComp;
1173  break;
1174  case "date":
1175  $aComp= strtotime($a[$comp["field"]]);
1176  $bComp= strtotime($b[$comp["field"]]);
1177  $comp= $comp["dir"] == "asc" ? $aComp - $bComp : $bComp - $aComp;
1178  break;
1179  // TODO: boolean, odata?
1180  }
1181 
1182  if ($comp != 0)
1183  return $comp;
1184  }
1185 }
1186 
1187 /**
1188  * function addSortFromData(&$data, &$sqls, &$errors, $sortCols, $sortDirs, $modelData)
1189  * This will sort after the database call. In the case of getting data from www3, www5, www6, conbining and then sorting.
1190  *
1191  * @param array $data -- the combined data
1192  * @param array $sqls -- the SQLs
1193  * @param array $errors -- the errors
1194  * @param array $sortCols -- the columns to sort.
1195  * @param array $sortDirs -- the directions of the columns to sort.
1196  * @param array $modelData -- the model data. This is included because if there isn't any sort, then the first column in the model is sorted.
1197  */
1198 function addSortFromData(&$data, &$sqls, &$errors, $sortCols, $sortDirs, $modelData)
1199 {
1200  $sortCols= trim($sortCols);
1201  $sortDirs= trim($sortDirs);
1202 
1203  if ($sortCols != "" && $sortDirs != "")
1204  {
1205  $colsExpanded= json_decode($sortCols, true);
1206  $dirsExpanded= json_decode($sortDirs, true);
1207  if (!is_array($colsExpanded))
1208  $errors[]= "Sort columns is not a valid array.";
1209  if (!is_array($dirsExpanded))
1210  $errors[]= "Sort directions is not a valid array.";
1211  if (count($colsExpanded) != count($dirsExpanded))
1212  $errors[]= "Sort arrays need to be the same size.";
1213  $sortSQL= array();
1214  $index= 0;
1215  $allValidDirs= true;
1216  $compArray= array();
1217  foreach($colsExpanded as $col)
1218  {
1219  $type= trim($modelData["fields"][$col]["type"]);
1220  $dir= strtolower(strval($dirsExpanded[$index]));
1221  if (in_array($dir, array("asc", "desc")) === false)
1222  $allValidDirs= false;
1223  $compArray[]= array("field" => $col, "dir" => $dir, "type" => $type);
1224  if (!$allValidDirs)
1225  break;
1226  $index++;
1227  }
1228  if (!$allValidDirs)
1229  $errors[]= "Sort directions must be either asc or desc.";
1230 
1231  if (count($sortSQL) != 0)
1232  $sql.= " order by " . implode(", ", $sortSQL);
1233  }
1234  else if (($sortCols != "" && $sortDirs == "") || ($sortCols == "" && $sortDirs != ""))
1235  $errors[]= "Both sort column and sort direction arrays must be defined for a sort.";
1236  else // There is always a sort. If there isn't one, then it is the first column asc
1237  {
1238  foreach($modelData as $kendoCol => $kendoColArray)
1239  {
1240  $compArray[]= array("field" => $kendoCol, "dir" => "asc", "type" => isset($kendoColArray["type"]) ? $kendoColArray["type"] : "string");
1241  break;
1242  }
1243  }
1244 
1245  usort($data, function($a, $b) use ($compArray) {
1246  return cmpSort($a, $b, $compArray);
1247  });
1248 }
1249 
1250 /**
1251  * function addPagingFromData(&$data, $limit, $offset)
1252  * This will set the current page after retrieving data. In the case of getting data from www3, www5, www6, conbining and then sorting.
1253  *
1254  * @param array $data -- the combined data
1255  * @param integer $limit -- pageSize
1256  * @param integer $offset -- where to start
1257  */
1258 function addPagingFromData(&$data, $limit, $offset)
1259 {
1260  $total= count($data);
1261  if ($limit != -1)
1262  $data= array_slice($data, $offset, $limit);
1263  $data= array("total" => $total, "data" => $data);
1264 }
1265 
1266 /**
1267  * function addFilter(&$sql, &$countSQL, &$errors, $filterCols, $filterOps, $filterValues, $modelData)
1268  * This will add the filter to current report SQL.
1269  *
1270  * @param string $sql -- the sql to get data.
1271  * @param string $countSQL -- the sql to get the count. (The filter needs to be added to both SQLs.)
1272  * @param array $errors -- the errors.
1273  * @param string $filterCols -- the columns to filter.
1274  * @param string $filterOps -- the operations of the columns to filter.
1275  * @param string $filterValues -- the values of the columns to filter.
1276  * @param array $modelData -- the model data. This is needed because the query is slightly different per data type.
1277  */
1278 function addFilter(&$sql, &$countSQL, &$errors, $filterCols, $filterOps, $filterValues, $modelData)
1279 {
1280  $filterCols= trim($filterCols);
1281  $filterOps= trim($filterOps);
1282  $filterValues= trim($filterValues);
1283  $filterSQL= array();
1284 
1285  if ($filterCols != "" && $filterOps != "" && $filterValues != "")
1286  {
1287  $colsExpanded= json_decode($filterCols, true);
1288  $opsExpanded= json_decode($filterOps, true);
1289  $valuesExpanded= json_decode($filterValues, true);
1290  if (!is_array($colsExpanded))
1291  $errors[]= "Filter columns is not a valid array.";
1292  if (!is_array($opsExpanded))
1293  $errors[]= "Filter operators is not a valid array.";
1294  if (!is_array($valuesExpanded))
1295  $errors[]= "Values is not a valid array.";
1296  if (!(count($colsExpanded) == count($opsExpanded) && count($opsExpanded) == count($valuesExpanded)))
1297  $errors[]= "Filter arrays need to be the same size.";
1298  $allValidCols= true;
1299  $allValidOps= true;
1300  $index= 0;
1301  $opStartMap= array("eq" => "= '", "neq" => "<> '", "lt" => "< '", "lte" => "<= '", "gt" => "> '", "gte" => ">= '", "startswith" => "like '", "contains" => "like '%",
1302  "endswith" => "like '%", "doesnotcontain" => "not like '%");
1303  $opEndMap= array("eq" => "'", "neq" => "'", "lt" => "'", "lte" => "'", "gt" => "'", "gte" => "'", "startswith" => "%'", "contains" => "%'", "endswith" => "'", "doesnotcontain" => "%'");
1304  $colMap= array();
1305 
1306  foreach($colsExpanded as $col)
1307  {
1308  $databaseCol= strtolower($col);
1309  $op= strtolower(strval($opsExpanded[$index]));
1310  $value= strtolower(strval($valuesExpanded[$index]));
1311  if (!isset($opStartMap[$op]))
1312  $allValidOps= false;
1313 
1314  switch($modelData[$col]["type"])
1315  {
1316  case "string":
1317  $filterSQL[]= "trim(from lower($databaseCol)) " . $opStartMap[$op] . trim($value) . $opEndMap[$op];
1318  break;
1319  case "number":
1320  case "date": // Handled on frontend
1321  $filterSQL[]= "$databaseCol " . $opStartMap[$op] . trim($value) . $opEndMap[$op];
1322  break;
1323  // TODO: boolean, odata?
1324  }
1325 
1326  if (!$allValidOps)
1327  break;
1328  $index++;
1329  }
1330  }
1331  else if (!($filterCols == "" && $filterOps == "" && $filterValues == ""))
1332  $errors[]= "All three filter arrays (Columns, operators, and values) must be defined for a filter.";
1333 
1334  $filterSQL= count($filterSQL) == 0 ? "" : " where (" . implode(") and (", $filterSQL) . ")";
1335  $sql.= $filterSQL;
1336  $countSQL.= $filterSQL;
1337 }
1338 
1339 /**
1340  * function readReportCommon($report, &$errors, &$sqls, $dbh, $limit, $offset, $includeSchema, $includeCount, $excludeData, $dontIncludeId, $excludeSort, $sortCols, $sortDirs,
1341  * $filterCols, $filterOps, $filterValues, $reportList, $defaultDateFormat, $cookieName)
1342  *
1343  * This reads the report and returns the actual data plus the kendo model and column attributes. This is the main entry point of this script.
1344  *
1345  * @param string $report -- the key of the report in the report definition global variable.
1346  * @param array $errors -- the errors.
1347  * @param array $sqls -- the sqls.
1348  * @param integer $dbh -- the database connection.
1349  * @param integer $limit -- the limit parameter (page size).
1350  * @param integer $offset -- the offset parameter (what page?).
1351  * @param boolean $includeSchema -- if true, include model data.
1352  * @param boolean $includeCount -- if true, also run the count query. For sort, this is unnecessary as the count stays the same. It is necessary for if the filter changes or a force refresh.
1353  * @param boolean $excludeData -- if true, then the query to get the data won't actually be run. I have the reports firing out two calls at initialization because I need the model and column data.
1354  * @param boolean $dontIncludeId -- if true, there won't be an additional id column for the kendo grid. This is for CSV.
1355  * @param boolean $excludeSort -- if true, ignores the sort parameters if needed to combine data from www3, www5, www6 first.
1356  * @param string $sortCols -- list of columns to sort.
1357  * @param string $sortDirs -- list of directions of the columns to sort.
1358  * @param string $filterCols -- list of columns to filter.
1359  * @param string $filterOps -- list of operations of columns to filter.
1360  * @param string $filterValues -- list of values of columns to filter.
1361  * @param array $reportList -- the global report list definition.
1362  * @param string $defaultDateFormat -- the default date format.
1363  * @param string $cookieName -- the cookie name. Only relevant if there is a report with the replaceString and replaceCode values set.
1364  *
1365  * @return array --
1366  * array $sql -- the SQLs used.
1367  * array $errors -- the errors.
1368  * array $reportData -- the data of the report.
1369  * array $modelData -- the array needed for the kendo grid's dataSource > schema > model attribute.
1370  * array $columnData -- the array needed for the kendo grid's column attribute.
1371  */
1372 function readReportCommon($report, &$errors, &$sqls, $dbh, $limit, $offset, $includeSchema, $includeCount, $excludeData, $dontIncludeId, $excludeSort, $sortCols, $sortDirs,
1373  $filterCols, $filterOps, $filterValues, $reportList, $defaultDateFormat, $cookieName)
1374 {
1375  $reportRecord= $reportList[$report];
1376  validateReportRecord($reportRecord, $errors);
1377 
1378  if (count($errors) == 0)
1379  {
1380  $baseSQL= $reportRecord["sql"];
1381  $colList= array();
1382 
1383  $modelData= array();
1384  $columnData= array();
1385 
1386  $dateFormat = isset($reportRecord["dateFormat"]) ? $reportRecord["dateFormat"] : "";
1387  $dateEmpty = isset($reportRecord["dateEmpty"]) ? $reportRecord["dateEmpty"] : "";
1388  $replaceString = isset($reportRecord["replaceString"]) ? $reportRecord["replaceString"] : null;
1389  $replaceCode = isset($reportRecord["replaceCode"]) ? $reportRecord["replaceCode"] : null;
1390  $ddl = isset($reportRecord["ddl"]) ? $reportRecord["ddl"] : "";
1391 
1392  getSchemaUsingBaseSQL($dbh, $baseSQL, $dateFormat, $dateEmpty, $reportRecord["cols"], $replaceString, $replaceCode,
1393  $ddl, $sqls, $errors, $modelData, $columnData, $includeSchema, $report, $defaultDateFormat, $cookieName);
1394 
1395  $fullSQL= "select * from ($baseSQL) t";
1396  $countSQL= "select count(*) from ($baseSQL) t";
1397  // If there are multiple servers, then we do still want to add the filter to the query.
1398  addFilter($fullSQL, $countSQL, $errors, $filterCols, $filterOps, $filterValues, $modelData);
1399 
1400  if (!$excludeSort) // If there are multiple servers, then this function is unnecessary.
1401  addSort($fullSQL, $errors, $sortCols, $sortDirs, $modelData);
1402 
1403  if ($limit != -1)
1404  {
1405  $fullSQL.= " limit $limit offset $offset";
1406  }
1407 
1408  }
1409  $reportData= array();
1410 
1411  if (count($errors) == 0)
1412  {
1413  getData($dbh, $fullSQL, $reportData, $modelData, $columnData, $sqls, $errors, $dontIncludeId, $excludeData);
1414  }
1415 
1416  $reportData= array("total" => -1, "data" => $reportData);
1417  if ($includeCount)
1418  {
1419  $sql= $countSQL;
1420  $sqls[]= $sql;
1421  $sth = db_query($sql,$dbh);
1422  $newError= trim(db_last_error());
1423  if ($newError != "")
1424  {
1425  $errors[]= $newError;
1426  }
1427  else
1428  {
1429  list($count) = db_fetch_array($sth,0);
1430  $reportData["total"]= $count;
1431  }
1432  db_free_result($sth);
1433  }
1434  return array("sql" => $sqls, "error" => $errors, "reportData" => $reportData, "modelData" => $modelData, "columnData" => $columnData);
1435 }
1436 
1437 /**
1438  * function runReadReportCurl($server, $parameters, $showSQL, $cookieName="")
1439  *
1440  * Reads the readReport operation through a curl call.
1441  *
1442  * @param string $server -- e.g. www4, www3, www.
1443  * @param array $parameters -- additional parameters.
1444  * @param boolean $showSQL -- if true, show the SQL from the curl call.
1445  * @param string $cookieName -- cookieName is relevant if one report has the replaceString and replaceCode attributes set.
1446  *
1447  * @return runCurl contents
1448  */
1449 function runReadReportCurl($server, $parameters, $showSQL, $cookieName="")
1450 {
1451  global $dataFile;
1452  $cmd = "https://${server}.homecu.net${dataFile}?operation=readReport";
1453  if ($showSQL)
1454  $cmd.= "SQL"; // Flips on the show SQL flag for the remote call so SQL can be shown from devel but not prod and so that I don't have to decode the json and then encode it right away.
1455  return runCurl($server, $parameters, $showSQL, $cmd, $cookieName);
1456 }
1457 
1458 /**
1459  * function runCurl($server, $parameters, $showSQL, $cmd, $cookieName)
1460  *
1461  * Runs a curl call
1462  *
1463  * @param string $server -- e.g. www4, www3, www.
1464  * @param array $parameters -- additional parameters.
1465  * @param boolean $showSQL -- if true, show the SQL from the curl call.
1466  * @param string $cmd -- the URL to run.
1467  * @param string $cookieName -- cookieName is relevant if one report has the replaceString and replaceCode attributes set.
1468  *
1469  * @return string -- whatever was returned by the call.
1470  */
1471 function runCurl($server, $parameters, $cookieName)
1472 {
1473  global $dataFile;
1474  $remoteFile = substr($dataFile, 0, -4);
1475 
1476  $curlServers = array("www3", "www5", "www6");
1477 
1478  $curlCookies = "";
1479  $curlCookies .= "HCUTicket=" . urlencode($_COOKIE['HCUTicket']);
1480  $curlCookies .= ($cookieName != "" ? ";$cookieName=" . urlencode($_COOKIE[$cookieName]) : "");
1481 
1482  $cmd = "https://${server}.homecu.net${remoteFile}?operation=readReport";
1483 
1484  $ch=curl_init($cmd);
1485  curl_setopt($ch, CURLOPT_COOKIE, $curlCookies);
1486  curl_setopt($ch, CURLOPT_USERPWD,"nobody:no1home");
1487  curl_setopt($ch, CURLOPT_RETURNTRANSFER,1); # get response as string
1488  curl_setopt($ch, CURLOPT_POST, true);
1489  curl_setopt($ch, CURLOPT_POSTFIELDS, $parameters);
1490  curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
1491  curl_setopt($ch, CURLOPT_COOKIESESSION, true );
1492  curl_setopt($ch, CURLOPT_COOKIEJAR, "/tmp/reportCookie");
1493  curl_setopt($ch, CURLOPT_COOKIEFILE, "/tmp/reportCookie");
1494 
1495  $rawresp=curl_exec($ch);
1496 
1497  return $rawresp;
1498 }
1499 
1500 /**
1501  * function printCSVData($reportValues, $report, $reportTitle, $reportList, $defaultDateFormat, $delimiter=",")
1502  *
1503  * Downloads a CSV from the page.
1504  *
1505  * @param array $reportValues -- the data of the report.
1506  * @param string $report -- the key of the report which is in the report definition array.
1507  * @param string $reportTitle -- what to save the CSV as.
1508  * @param array $reportList -- the report definition array.
1509  * @param string $defaultDateFormat -- the default date format. Now, in the configuration, this is intrepreted as the PHP date format so this is translated into javascript date format.
1510  * @param string $delimiter -- what should separate the columns and column values? A Comma is assumed.
1511  */
1512 function printCSVData($reportValues, $report, $reportTitle, $reportList, $defaultDateFormat, $delimiter=",")
1513 {
1514  $reportRecord= $reportList[$report];
1515  $reportName= $reportTitle != "" ? $reportTitle : (isset($reportRecord["title"]) ? $reportRecord["title"] : trim($report));
1516  $kendoDateFormat= isset($reportRecord["dateFormat"]) ? $reportRecord["dateFormat"] : (isset($defaultDateFormat) ? $defaultDateFormat : "MM/dd/yyyy");
1517 
1518  $csvData= array();
1519  $csvRow= array();
1520 
1521  $phpDateFormatArray= array("yyyy" => "Y", "dd" => "d", "HH" => "H", "MM" => "m", "mm" => "i", "ss" => "s");
1522  $dateArray= array();
1523 
1524  $phpDateFormat= $kendoDateFormat;
1525  foreach($phpDateFormatArray as $from => $to)
1526  {
1527  $phpDateFormat= str_replace($from, $to, $phpDateFormat);
1528  }
1529 
1530  // This pushes the date format back in
1531  foreach($reportValues["modelData"]["fields"] as $kendoCol => $modelRecord)
1532  {
1533  if ($modelRecord["type"] == "date")
1534  $dateArray[$kendoCol]= true;
1535  }
1536 
1537  foreach($reportValues["columnData"] as $column)
1538  {
1539  $title= strval($column["title"]);
1540  if (strpos($title, "$delimiter") !== false)
1541  $title= "\"$title\"";
1542  $csvRow[]= $title;
1543  }
1544  $csvData[]= implode("$delimiter ", $csvRow);
1545 
1546  foreach($reportValues["reportData"]["data"] as $row)
1547  {
1548  $csvRow= array();
1549  foreach($row as $col => $value)
1550  {
1551  if (isset($dateArray[$col]))
1552  {
1553  try
1554  {
1555  $dateTime= new DateTime($value);
1556  $value= $dateTime->format($phpDateFormat);
1557  }
1558  catch (exception $e)
1559  {
1560  $value= "(Invalid Date Format)";
1561  }
1562  }
1563  else
1564  $value= strval($value);
1565 
1566  if (strpos($value, "$delimiter") !== false || strpos($value, "\n") !== false)
1567  $value= "\"$value\"";
1568  $csvRow[]= $value;
1569  }
1570  $csvData[]= implode("$delimiter ", $csvRow);
1571  }
1572  $csvData= implode("\r\n", $csvData);
1573 
1574  header("Content-length: " . strlen($csvData) );
1575  header("Content-type: application/octetstream");
1576  header("Content-disposition: inline; filename=\"$reportName.csv\"");
1577  print ($csvData);
1578 
1579 }
1580 
1581 /**
1582  * function validateReportRecord($reportRecord, &$errors)
1583  * Validates the $reportRecord to spot some obvious problems before actually parsing the SQL and adding sort and filter and whatnot.
1584  *
1585  * @param array $reportRecord -- the array that has all the information to create a report.
1586  * @param array $errors -- the errors.
1587  */
1588 function validateReportRecord($reportRecord, &$errors)
1589 {
1590  try
1591  {
1592  if (!isset($reportRecord["sql"]))
1593  throw new exception("SQL is required!", 1);
1594  if (!isset($reportRecord["cols"]))
1595  throw new exception("Cols are required!", 2);
1596  foreach($reportRecord["cols"] as $colName => $colType)
1597  {
1598  if (!in_array($colType, array("number", "string", "date", "boolean", "odata", "currency", "ignore", "list")))
1599  throw new exception("Column type is not known.", 3);
1600  if ($colType == "list")
1601  {
1602  if (!isset($reportRecord["ddl"]))
1603  throw new exception ("DDLs are not defined.", 4);
1604  if (!isset($reportRecord["ddl"][$colName]))
1605  throw new exception ("DDL is not defined for $colName.", 5);
1606  }
1607  }
1608  }
1609  catch(exception $e)
1610  {
1611  $errors[]= $e->getMessage();
1612  }
1613 }
getColumnSQL($toBeSummed)
Definition: reports.i:99
getColumns($columnArray)
Definition: reports.i:122
getCTE()
Definition: reports.i:76
__construct()
Definition: reports.i:44