Framework  3.9
data_import_manager.inc
Go to the documentation of this file.
1 <?php
2 /**************************************************************
3 
4  Copyright (c) 2007-2010 Sonjara, Inc
5 
6  Permission is hereby granted, free of charge, to any person
7  obtaining a copy of this software and associated documentation
8  files (the "Software"), to deal in the Software without
9  restriction, including without limitation the rights to use,
10  copy, modify, merge, publish, distribute, sublicense, and/or sell
11  copies of the Software, and to permit persons to whom the
12  Software is furnished to do so, subject to the following
13  conditions:
14 
15  The above copyright notice and this permission notice shall be
16  included in all copies or substantial portions of the Software.
17 
18  Except as contained in this notice, the name(s) of the above
19  copyright holders shall not be used in advertising or otherwise
20  to promote the sale, use or other dealings in this Software
21  without prior written authorization.
22 
23  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
24  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
25  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
26  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
27  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
28  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
29  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
30  OTHER DEALINGS IN THE SOFTWARE.
31 
32 *****************************************************************/
33 
71 require_once realpath(dirname(__FILE__))."/data_view.inc";
72 
74 {
76  var $id;
77  var $log;
78  var $objs = array();
79  var $import = false;
80  var $preview = false;
81  var $matches = array();
82  var $savedObjs = array();
83  var $onPreProcessRow = null;
84  var $filepath = null;
86  var $buttons = array();
87  var $onSaveComplete = null;
88  var $field = "csv_file";
89  var $cssClass = "";
90  var $table;
91  var $quiet = false;
92 
98  function __construct($class_name, $id = "")
99  {
100  $this->class_name = $class_name;
101  $this->id = ($id) ? $id : codify(strtolower($this->class_name)) . "_data_import";
102 
103  if($_POST["import"])
104  {
105  $this->import = true;
106  }
107  }
108 
118  function column($field, $position, $label = "", $options = array(), $importer = "", $update_empty = true)
119  {
120  if(!$label)
121  {
122  $obj = new $this->class_name;
123  $label = $obj->prettifyFieldName($field);
124  }
125  $this->columns[] = new DataImportColumn($field, $position, $label, $options, $importer, $update_empty);
126 
127  return $this;
128  }
129 
140  function additional($field, $label = "", $template = "", $update_empty)
141  {
142  if(!$label)
143  {
144  $obj = new $this->class_name;
145  $label = $obj->prettifyFieldName($field);
146  }
147  $this->columns[] = new DataImportColumn($field, 0, $label, null, $template, $update_empty);
148 
149  return $this;
150  }
151 
152  function match()
153  {
154  foreach(func_get_args() as $field)
155  {
156  $this->matches[$field] = true;
157  }
158  }
159 
168  function formatList($items, $nameField)
169  {
170  if(count($items) == 0)
171  {
172  return array();
173  }
174 
175  $out = array();
176  foreach($items as $item)
177  {
178  $pk = $item->getPrimaryKey();
179  $out[$item->$pk] = $item->$nameField;
180  }
181 
182  return $out;
183  }
184 
188  function import()
189  {
190  $fp = $this->openFile();
191  if(!$fp) return;
192 
193  if($this->loadData($fp))
194  {
195  $this->save();
196  if (!$this->quiet)
197  {
198  $this->drawButtons();
199  $this->writeLog();
200  }
201  }
202  }
203 
213  function button($text, $url, $confirm = null, $isScript = false)
214  {
215  $this->buttons[] = array('text' => $text, 'url' => $url, 'confirm' => $confirm, 'isScript' => $isScript);
216  }
217 
222  function loadData($fp)
223  {
224  trace("Loading Data...", 3);
225 
226  // Use for indexing warnings
227  $row_number = 1;
228  $first = true;
229  $read = 0;
230 
231  while (($row = fgetcsv($fp)) !== FALSE)
232  {
233  $read++;
234  if($first && $this->validateDataFile && !call_user_func($this->validateDataFile, $row))
235  {
236  return false;
237  }
238  $first = false;
239  $obj = $this->importOneRow($row, $row_number);
240  if($obj)
241  {
242  $this->objs[$obj->row_number] = $obj;
243  $row_number++;
244  }
245  }
246 
247  trace("Read {$row_number} objects from {$read} rows", 3);
248  return true;
249  }
250 
256  function log($text)
257  {
258  $this->log .= $text . "</br></br>";
259  }
260 
264  function writeLog()
265  {
266  if (!$this->quiet) echo $this->log;
267  }
268 
276  function importOneRow($row, $row_number)
277  {
279  $obj = new $class_name;
280 
281  $this->init($obj, $row_number);
282 
283  foreach($this->columns as $column)
284  {
285  $position = $column->position;
286  if($position == 0) continue;
287  $this->importCell($column, $obj, $row[$position-1]);
288  }
289 
290  // Load additional columns after in case their value depends on rows
291  // loaded from spreadsheet
292  foreach($this->columns as $column)
293  {
294  if($column->position > 0) continue;
295  $this->importCell($column, $obj, 0);
296  }
297 
298  $obj = $this->preProcessRow($obj);
299 
300  return $obj;
301  }
302 
310  function preProcessRow($obj)
311  {
312  if(count($this->matches))
313  {
314  $savedObj = $this->findMatch($obj);
315  if($savedObj)
316  {
317  $pk = $this->getPrimaryKey();
318  $this->savedObjs[$savedObj->$pk] = $savedObj;
319  $obj->$pk = $savedObj->$pk;
320  $this->setFilter($obj, $savedObj);
321  if(!$obj->getFilter()) return null;
322  }
323  }
324 
325  if ($this->onPreProcessRow)
326  {
327  call_user_func($this->onPreProcessRow, $obj, $savedObj);
328  }
329 
330  return $obj;
331  }
332 
341  function getSavedObj($obj)
342  {
343  $pk = $this->getPrimaryKey();
344 
345  if(array_key_exists($obj->$pk, $this->savedObjs))
346  {
347  return $this->savedObjs[$obj->$pk];
348  }
349 
350  return null;
351  }
352 
359  function findMatch($obj)
360  {
361  if(!count($this->matches))
362  {
363  return null;
364  }
365 
366  foreach($this->matches as $field => $dump)
367  {
368  $constraint[] = $field . "=:$field";
369  }
370 
371  $query = Query::create($this->class_name, "WHERE " . implode(" AND ", $constraint));
372 
373  foreach($this->matches as $field => $dump)
374  {
375  $query->bind(":$field", $obj->$field);
376  }
377 
378  $foundObjs = $query->execute();
379 
380  return (count($foundObjs)) ? $foundObjs[0] : null;
381  }
382 
391  function setFilter($obj, $old)
392  {
393  $fields = $obj->getFields();
394  $changed = false;
395 
396  $obj->filter = new InclusionFilter();
397  $old->filter = new InclusionFilter();
398  foreach($fields as $field => $type)
399  {
400  $column = $this->getColumn($field);
401  if(!$column) continue;
402  $value = $obj->$field;
403  if(($value || $column->update_empty) && $obj->$field != $old->$field)
404  {
405  $changed = true;
406  $obj->filter->add($field);
407  $old->filter->add($field);
408  }
409  }
410 
411  if(!$changed)
412  {
413  $obj->filter = null;
414  $old->filter = null;
415  }
416  }
417 
418  function getColumn($field)
419  {
420  if(!count($this->columns)) return false;
421 
422  if(count($this->columns))
423  {
424  foreach($this->columns as $column)
425  {
426  if($column->field == $field)
427  {
428  return $column;
429  }
430  }
431  }
432 
433  return null;
434  }
435 
436  /*
437  * Initialize any fields that may not get a value
438  * in the import.
439  *
440  * e.g. set user_id, date_created, etc.
441  */
442  function init(&$obj, $row_number)
443  {
444  // use for indexing
445  $obj->set("row_number", $row_number);
446  }
447 
448  function save()
449  {
450  if(count($this->objs) == 0)
451  {
452  return;
453  }
454 
455  foreach($this->objs as $row_number => $obj)
456  {
457  $pk = $this->getPrimaryKey();
458  $new = false;
459 
460  if(!$obj->$pk)
461  {
462  $new = true;
463  }
464  $obj->save();
465  $this->log($this->formatSaveLogText($row_number, $obj, $new));
466  }
467 
468  if ($this->onSaveComplete)
469  {
470  call_user_func_array($this->onSaveComplete, array($this));
471  }
472 
473  return true;
474  }
475 
485  function formatSaveLogText($row_number, $obj, $new)
486  {
487  $pk = $this->getPrimaryKey();
488  $name = $this->getClassName();
489  $fields = $obj->getFields();
490  $out = array();
491 
492  if(!$new)
493  {
494  $filter = $obj->getFilter();
495  foreach($fields as $field => $type)
496  {
497  if($filter && !$filter->isExcluded($field))
498  {
499  $out[] = $field . " " . $obj->$field;
500  }
501  }
502  $text = "<b>Updated</b> $name from row number $row_number $pk {$obj->$pk} " . implode(" ", $out);
503  }
504  else
505  {
506  foreach($fields as $field => $type)
507  {
508  $out[] = $field . " " . $obj->$field;
509  }
510 
511  $text = "<b>Inserted</b> into $name from row number $row_number $pk {$obj->$pk} " . implode(" ", $out);
512  }
513 
514  foreach($this->columns as $column)
515  {
516  $warning = $column->getWarning($obj);
517  if($warning)
518  {
519  $text .= "<span class='warning'>{$warning}</warning>";
520  }
521  }
522 
523  return $text;
524  }
525 
526  function getPrimaryKey()
527  {
529  $obj = new $class_name;
530  return $obj->getPrimaryKey();
531  }
532 
537  function getClassName()
538  {
540  $obj = new $class_name;
541  return $obj->prettifyClassName();
542  }
543 
548  function buildInputTable()
549  {
550  $table = new DataListView($this->columns, $this->id);
551  $table->column("Column Name", "{field}")
552  ->column("Title/Label", "{label}")
553  ->column("Options", "{formatOptions()}")
554  ->column("Position", "{position}")
555  ;
556 
557  $table->sortable = false;
558  $table->paginate = false;
559  $table->filter = false;
560 
561  return $table;
562  }
563 
564  function writeScript()
565  {
566  return "";
567  }
568 
569  function drawView()
570  {
571  $file = $this->getFilePath();
572 
573  $class = ($this->cssClass) ? " class='{$this->cssClass}'" : "";
574  echo "<div id='data_import'{$class}>\n";
575 
576  if(!$file)
577  {
578  $this->drawForm();
579  }
580  else if(!$this->import)
581  {
582  $this->preview();
583  }
584  else
585  {
586  $this->import();
587  }
588  echo "</div>\n";
589  }
590 
594  function preview()
595  {
596  $this->preview = true;
597  $this->uploadDataImportFile();
598 
599  $fp = $this->openFile();
600  if(!$fp) return;
601 
602  if($this->loadData($fp))
603  {
604  $this->table = $this->buildPreviewTable();
605  }
606 
607  if(count($this->table->items))
608  {
609  echo "<form method='POST' action='' enctype='multipart/form-data'>\n";
610  echo "<input type='hidden' name='{$this->field}' value='{$this->filepath}'/>\n";
611  $this->drawButtons();
612  }
613 
614  echo $this->log;
615  $this->table->drawView();
616 
617  if(!count($this->table->items) && !$this->filepath)
618  {
619  $this->drawForm();
620  }
621 
622  $this->drawButtons();
623 
624  if(count($this->table->items))
625  {
626  echo "</form>\n";
627  }
628  }
629 
634  function drawForm()
635  {
636  $table = $this->buildInputTable();
637  echo "<p><label>Description of columns expected in import file:</label></p>\n";
638  $table->drawView();
639 
640  echo "</br></br><form method='POST' action='' enctype='multipart/form-data'>
641  <label>CSV file to import: </label><input type='file' name='{$this->field}'/><br/><br/>
642  <input type='submit' name='import' class='button' value='Import File'/>
643  <input type='submit' name='preview' class='button' value='Preview Import'/>
644  </form>";
645  }
646 
648  {
649  global $config;
650 
651  $file = $_FILES[$this->field]["tmp_name"];
652 
653  if(!$file)
654  {
655  return;
656  }
657 
658  $fullpath = $config["uploadbase"] . DIRECTORY_SEPARATOR . basename($file);
659 
660  if (file_exists($fullpath))
661  {
662  unlink($fullpath);
663  }
664 
665  trace("Uploading $file to $fullpath", 3);
666 
667  move_uploaded_file($file, $fullpath);
668  chmod($fullpath, 0755);
669 
670  $this->filepath = $fullpath;
671  }
672 
677  function drawButtons()
678  {
679  $buttons = array();
680 
681  foreach($this->buttons as $button)
682  {
683  $url = ($button['isScript']) ? $button['url'] : "go('{$button['url']}');";
684 
685  if ($button['confirm'])
686  {
687  $link = "if (confirm('".jsSafe($button['confirm'])."')) $url; return false;";
688  }
689  else
690  {
691  $link = "$url; return false;";
692  }
693 
694  if(preg_match("/import=1/", $url) && !count($this->objs))
695  {
696  continue;
697  }
698  $buttons[] = "<input type='button' class='{$this->buttonCSS}' onclick=\"$link\" value=\"{$button['text']}\"/>";
699  }
700 
701  if($this->preview && $this->table && count($this->table->items))
702  {
703  array_unshift($buttons, "<input type='submit' name='import' class='button' value='Import File'/>\n");
704  }
705 
706  echo "<div class='button_row'>" . implode("&nbsp;&nbsp;", $buttons) . "</div>";
707  }
708 
709  function openFile()
710  {
711  $filepath = $this->getFilePath();
712  $fp = fopen($filepath, 'r');
713 
714  if (fgetcsv($fp) == FALSE)
715  {
716  echo "<div class='error'>Cannot open data file {$filepath}.</div>\n";
717  return null;
718  }
719 
720  return $fp;
721  }
722 
729  function getFilePath()
730  {
731  if($this->filepath)
732  {
733  return $this->filepath;
734  }
735  else if($_FILES[$this->field]["tmp_name"])
736  {
737  return $_FILES[$this->field]["tmp_name"];
738  }
739  else if($_POST[$this->field])
740  {
741  return $_POST[$this->field];
742  }
743 
744  return null;
745  }
746 
752  function buildPreviewTable()
753  {
754  $pObjs = $this->createPreviewData();
755 
756  $pk = $this->getPrimaryKey();
757  $table = new DataListView($pObjs, $this->id);
758  $table->column("Row", array($this, formatRowIndex));
759  $table->column(prettify($pk), "{{$pk}}");
760 
761  foreach($this->columns as $column)
762  {
763  $field = $column->field;
764  $table->column($column->label, "{{$field}}");
765  }
766 
767  $table->onStartRow = array($this, previewRowStyle);
768  $table->emptyMessage = "No data to be imported.";
769  $table->sortable = false;
770  $table->paginate = false;
771  $table->filter = false;
772 
773  return $table;
774  }
775 
776  function previewRowStyle($obj)
777  {
778  $pk = $this->getPrimaryKey();
779  if(!$obj->$pk)
780  {
781  return "data_import_row_new";
782  }
783  }
784 
792  function createPreviewData()
793  {
794  $filter = new InclusionFilter();
795  foreach($this->columns as $column)
796  {
797  $filter->add($column->field);
798  }
799 
800  $pObjs = array();
801  if(count($this->objs))
802  {
803  foreach($this->objs as $obj)
804  {
805  $pObj = clone $obj;
806  $pObj->filter = $filter;
807  foreach($this->columns as $column)
808  {
809  $pObj->filter->add($column->field);
810  $pObj->set($column->field, $this->formatPreviewCell($pObj, $column));
811  }
812 
813  $pObjs[] = $pObj;
814  }
815  }
816 
817  return $pObjs;
818  }
819 
828  function formatPreviewCell($obj, $column)
829  {
830  $old = $this->getSavedObj($obj);
831 
832  if(is_callable($column->preview_template))
833  {
834  return call_user_func($column->preview_template, $obj, $old);
835  }
836  }
837 
838 
851  function importCell($column, $obj, $value)
852  {
853  if (is_callable($column->importer) && $column->position > 0)
854  {
855  return call_user_func($column->importer, $obj, $value);
856  }
857  else if (is_callable($column->importer) && $column->position == 0)
858  {
859  return call_user_func($column->importer, $obj);
860  }
861 
862  else $column->import($obj, $value);
863  }
864 
865  function formatRowIndex($obj)
866  {
867  return $obj->row_number;
868  }
869 }
870 
872 {
873  var $fields = array(
874  "field" => String,
875  "label" => String,
876  "position" => Number,
877  "update_empty" => Boolean,
878  );
879 
880  var $options = array();
881  var $importer;
882  var $template;
884  var $warnings = array();
885 
896  function __construct($field, $position, $label = "", $options, $importer, $update_empty = true)
897  {
898  $this->primary_key = "field";
899  $this->table = "import_column";
900 
901  $this->field = $field;
902  $this->label = $label;
903  $this->position = $position;
904  $this->importer = $importer;
905  $this->options = $options;
906  $this->update_empty = $update_empty;
907 
908  if($this->options && !$importer)
909  {
910  $this->importer = array($this, importOption);
911  }
912 
913  if($this->options)
914  {
915  $this->preview_template = array($this, formatOptionPreview);
916  }
917  else
918  {
919  $this->preview_template = array($this, formatPreview);
920  }
921 
922  $this->template = "{{$field}}";
923  }
924 
933  function formatOptionPreview($obj, $old)
934  {
935  $field = $this->field;
936 
937  $value = $this->startFormatPreview($obj, $old, $field);
938 
939  if(!$value)
940  {
941  $value .= $this->options[$obj->$field] . " (" . $obj->format($this->template) . ")";
942  }
943 
944  $value .= $this->endFormatPreview($old, $field);
945 
946  return $value;
947  }
948 
959  function startFormatPreview($obj, $old, $field)
960  {
961  $warning = $this->getWarning($obj);
962  if($warning)
963  {
964  $obj->fields[$field] = HTML;
965  $value = "<span class='warning'>{$warning}</warning>\n";
966  }
967 
968  if($this->isChanged($old, $field))
969  {
970  $obj->fields[$field] = HTML;
971  $value .= "<b>";
972  }
973 
974  return $value;
975  }
976 
977  function getWarning($obj)
978  {
979  if(array_key_exists($obj->row_number, $this->warnings))
980  {
981  return $this->warnings[$obj->row_number];
982  }
983 
984  return "";
985  }
986 
995  function endFormatPreview($old, $field)
996  {
997  if($this->isChanged($old, $field))
998  {
999  $value .= "</b></br><i>" . $old->format($this->template) . "</i>";
1000  }
1001 
1002  return $value;
1003  }
1004 
1015  function isChanged($old, $field)
1016  {
1017  if(!$old) return false;
1018 
1019  $filter = $old->getFilter();
1020  if($filter && !$filter->isExcluded($field))
1021  {
1022  return true;
1023  }
1024 
1025  return false;
1026  }
1027 
1035  function formatPreview($obj, $old)
1036  {
1037  $field = $this->field;
1038 
1039  $value = $this->startFormatPreview($obj, $old, $field);
1040 
1041  $value .= $obj->format($this->template);
1042 
1043  $value .= $this->endFormatPreview($old, $field);
1044 
1045  return $value;
1046  }
1047 
1056  function importOption($obj, $value)
1057  {
1059  if(!count($options)) return;
1060  $label = $this->label;
1061 
1062  $options = array_flip($this->options);
1063 
1064  if(array_key_exists($value, $options))
1065  {
1066  $obj->$field = $options[$value];
1067  }
1068  else
1069  {
1070  $this->warning[$obj->row_number] = "{$label} $value not found";
1071  }
1072  }
1073 
1078  function formatOptions()
1079  {
1080  if(!count($this->options))
1081  {
1082  return "N/A";
1083  }
1084 
1085  foreach($this->options as $key => $value)
1086  {
1087  $out[] = $value;
1088  }
1089  $out = implode("<br>", $out);
1090 
1091  return "<div class='scrollbox' style='height: 200px; overflow: scroll; border: none'>$out</div>";
1092  }
1093 
1100  function import($obj, $value)
1101  {
1102  $field = $this->field;
1103  $obj->$field = $value;
1104  }
1105 }
$template
formatting template for preview display of the data item field
formatOptionPreview($obj, $old)
For fields whose value is derived from a provided options array, format the display of the column in ...
isChanged($old, $field)
Determine if the value has changed.
formatPreview($obj, $old)
Default preview table formatter.
$warnings
store warnings indexed by the object row number pseudo field
endFormatPreview($old, $field)
If the value has changed, show the old value in the table preview.
__construct($field, $position, $label="", $options, $importer, $update_empty=true)
Creates a data import column definition.
$importer
optional callback to handle customized login on import of a column
formatOptions()
Called by input table - show the set of options that represent valid input for the column.
$preview_template
formatting template for preview display of the entire cell
startFormatPreview($obj, $old, $field)
If the value has changed, show the new value in bold.
importOption($obj, $value)
If no customized callback function for setting the columns value on import is provided for a column w...
$options
optional - from related table or DataItem array whose value must match imported value
Import data from a cvs file.
createPreviewData()
Clone each of the objs that would be saved to the db on import and set into a preview object that has...
preProcessRow($obj)
If implementing classes may wish to add custom logic for handling a row before it is saved or display...
importCell($column, $obj, $value)
Given the input value read in, set the value into the field of the object.
$buttons
The custom buttons collection.
getClassName()
Get the display (prettified) version of the class name for the import.
$matches
Field(s) to use to check for a match in db - can be empty.
importOneRow($row, $row_number)
Given a data row, set the value into each field of the DataItem object.
$validateDataFile
optional callback to validate data contents of the file
$class_name
name of data item class to be updated
$objs
Contains data read in from the input file.
getSavedObj($obj)
Called by the build preview table method.
buildInputTable()
Describe the expected columns to the user above the Browse File form.
formatPreviewCell($obj, $column)
When building the preview table, format the column using the supplied DataImportColumn preview templa...
log($text)
Store info, errors, and warnings in a log file and ouput.
$savedObjs
Matches found in the db for data in the import file, indexed by primary key.
$onSaveComplete
Callback event handler that is fired after all the imported rows are saved.
drawButtons()
Draws any additional buttons specified in the calling script.
findMatch($obj)
Check the db for a record that matches the values for the field(s) given in the matches array.
loadData($fp)
Load the data from the spreadsheet (cvs file) into the DataItem objects.
getFilePath()
Implementing classes may wish to retrieve differently (e.g., document library)
button($text, $url, $confirm=null, $isScript=false)
Adds a custom button to the form.
$quiet
Quiet mode - generate no output to the HTML stream.
preview()
Load the data into preview objects and display in a table.
$preview
whether we are in preview mode
setFilter($obj, $old)
If a match to a saved obj is found, set the filter to include only those fields that have changed.
__construct($class_name, $id="")
formatList($items, $nameField)
When instantiating a DataImportColumn, use this function to build an array of option key/value pairs ...
drawForm()
Display the expected input column layout and show a File upload renderer for the user to select the c...
buildPreviewTable()
Display in a DataListView the values read in and set into the data objects and show a Warnings column...
formatSaveLogText($row_number, $obj, $new)
For each row saved, output text stating the row was updated and which fields set.
writeLog()
Output the log.
$filepath
optionally set known file path for csv file; otherwise provide form for user to specify
additional($field, $label="", $template="", $update_empty)
If values are set using logic that does not refer to data in a particular column, use additional with...
init(&$obj, $row_number)
column($field, $position, $label="", $options=array(), $importer="", $update_empty=true)
Adds a data import column definition.
$onPreProcessRow
callback for after each row of data is loaded and before it is saved or displayed in preview table
DataItem is the generic base class for database mapped classes.
Definition: data_item.inc:62
DataListView displays a list of DataItems (or InnerJoinResults) in tabular format.
Definition: data_view.inc:56
Used to place a filter on the contents of a DataItem-derived object.
static create($class, $constraints="")
Static factory method to create a new Query.
Definition: query.inc:358
codify($name)
Takes a text string and converts it into a code-compliant format, suitable for use as a variable name...
Definition: functions.inc:1399
trace($msg, $lvl=3, $callStack=null)
Send output to the trace log.
Definition: functions.inc:1010
prettify($name)
Takes a variable or field name and converts it into a human-readable version (assuming that the origi...
Definition: functions.inc:1413