Overview

Packages

  • awl
    • AuthPlugin
    • AwlDatabase
    • Browser
    • classEditor
    • DataEntry
    • DataUpdate
    • EMail
    • iCalendar
    • MenuSet
    • PgQuery
    • Session
    • Translation
    • User
    • Utilities
    • Validation
    • vCalendar
    • vComponent
    • XMLDocument
    • XMLElement
  • None
  • PHP

Classes

  • Browser
  • BrowserColumn
  • Overview
  • Package
  • Class
  • Tree
  • Deprecated
  • Todo
  1: <?php
  2: /**
  3: * Table browser / lister class
  4: *
  5: * Browsers are constructed from BrowserColumns and can support sorting
  6: * and other interactive behaviour.  Cells may contain data which is
  7: * formatted as a link, or the entire row may be linked through an onclick
  8: * action.
  9: *
 10: * @package   awl
 11: * @subpackage   Browser
 12: * @author    Andrew McMillan <andrew@mcmillan.net.nz>
 13: * @copyright Catalyst IT Ltd, Morphoss Ltd <http://www.morphoss.com/>
 14: * @license   http://gnu.org/copyleft/gpl.html GNU GPL v2 or later
 15: */
 16: 
 17: require_once("AWLUtilities.php");
 18: 
 19: /**
 20: * Ensure that this is not set elsewhere.
 21: */
 22: $BrowserCurrentRow = (object) array();
 23: 
 24: 
 25: 
 26: /**
 27: * BrowserColumns are the basic building blocks.  You can specify just the
 28: * field name, and the column header or you can get fancy and specify an
 29: * alignment, format string, SQL formula and cell CSS class.
 30: * @package   awl
 31: */
 32: class BrowserColumn
 33: {
 34:   var $Field;
 35:   var $Header;
 36:   var $Format;
 37:   var $Sql;
 38:   var $Align;
 39:   var $Class;
 40:   var $Type;
 41:   var $Translatable;
 42:   var $Hook;
 43:   var $current_row;
 44: 
 45:   /**
 46:   * BrowserColumn constructor.  Only the first parameter is mandatory.
 47:   *
 48:   * @param string field The name of the column in the SQL result.
 49:   * @param string header The text to appear in the column header on output
 50:   *                      (@see BrowserColumn::RenderHeader()).  If this is not supplied then
 51:   *                      a default of the field name will be used.
 52:   * @param string align left|center|right - text alignment.  Defaults to 'left'.
 53:   * @param string format A format (a-la-printf) to render data values within.
 54:   *                      (@see BrowserColumn::RenderValue()).  If this is not supplied
 55:   *                      then the default will ensure the column value is displayed as-is.
 56:   * @param string sql Some SQL which will return the desired value to be presented as column 'field' of
 57:   *                   the result. If this is blank then the column is assumed to be a real data column.
 58:   * @param string class Additional classes to apply to the column header and column value cells.
 59:   * @param string datatype This will allow 'date' or 'timestamp' to preformat the field correctly before
 60:   *                        using it in replacements or display.  Other types may be added in future.
 61:   * @param string $hook The name of a global function which will preprocess the column value
 62:   *
 63:   * The hook function should be defined as follows:
 64:   *   function hookfunction( $column_value, $column_name, $database_row ) {
 65:   *     ...
 66:   *     return $value;
 67:   *   }
 68:   */
 69:   function BrowserColumn( $field, $header="", $align="", $format="", $sql="", $class="", $datatype="", $hook=null ) {
 70:     $this->Field  = $field;
 71:     $this->Sql    = $sql;
 72:     $this->Header = $header;
 73:     $this->Format = $format;
 74:     $this->Class  = $class;
 75:     $this->Align  = $align;
 76:     $this->Type   = $datatype;
 77:     $this->Translatable = false;
 78:     $this->Hook   = $hook;
 79:   }
 80: 
 81:   /**
 82:   * GetTarget
 83:   *
 84:   * Retrieves a 'field' or '...SQL... AS field' definition for the target list of the SQL.
 85:   */
 86:   function GetTarget() {
 87:     if ( $this->Sql == "" ) return $this->Field;
 88:     return "$this->Sql AS $this->Field";
 89:   }
 90: 
 91:   /**
 92:   * RenderHeader
 93:   * Renders the column header cell for this column.  This will be rendered as a <th>...</th>
 94:   * with class and alignment applied to it.  Browser column headers are clickable, and the
 95:   * ordering will also display an 'up' or 'down' triangle with the column header that the SQL
 96:   * is sorted on at the moment.
 97:   *
 98:   * @param string order_field The name of the field currently being sorted on.
 99:   * @param string order_direction Whether the sort is Ascending or Descending.
100:   * @param int browser_array_key Used this to help handle separate ordering of
101:   *                              multiple browsers on the same page.
102:   * @param string forced_order If true, then we don't allow order to be changed.
103:   */
104:   function RenderHeader( $order_field, $order_direction, $browser_array_key=0, $forced_order=false ) {
105:     global $c;
106:     if ( $this->Align == "" ) $this->Align = "left";
107:     $html = '<th class="'.$this->Align.'" '. ($this->Class == "" ? "" : "class=\"$this->Class\"") . '>';
108: 
109:     $direction = 'A';
110:     $image = "";
111:     if ( !$forced_order && $order_field == $this->Field ) {
112:       if ( strtoupper( substr( $order_direction, 0, 1) ) == 'A' ) {
113:         $image = 'down';
114:         $direction = 'D';
115:       }
116:       else {
117:         $image = 'up';
118:       }
119:       $image = "<img class=\"order\" src=\"$c->images/$image.gif\" alt=\"$image\" />";
120:     }
121:     if ( !isset($browser_array_key) || $browser_array_key == '' ) $browser_array_key = 0;
122:     if ( !$forced_order ) $html .= '<a href="'.replace_uri_params( $_SERVER['REQUEST_URI'], array( "o[$browser_array_key]" => $this->Field, "d[$browser_array_key]" => $direction ) ).'" class="order">';
123:     $html .= ($this->Header == "" ? $this->Field : $this->Header);
124:     if ( !$forced_order ) $html .= "$image</a>";
125:     $html .= "</th>\n";
126:     return $html;
127:   }
128: 
129:   function SetTranslatable() {
130:     $this->Translatable = true;
131:   }
132: 
133:   function RenderValue( $value, $extraclass = "" ) {
134:     global $session;
135: 
136:     if ( $this->Type == 'date' || $this->Type == 'timestamp') {
137:       $value = $session->FormattedDate( $value, $this->Type );
138:     }
139: 
140:     if ( $this->Hook && function_exists($this->Hook) ) {
141:       dbg_error_log( "Browser", ":Browser: Hook for $this->Hook on column $this->Field");
142:       $value = call_user_func( $this->Hook, $value, $this->Field, $this->current_row );
143:     }
144: 
145:     if ( $this->Translatable ) {
146:       $value = translate($value);
147:     }
148: 
149:     $value = str_replace( "\n", "<br />", $value );
150:     if ( substr(strtolower($this->Format),0,3) == "<td" ) {
151:       $html = sprintf($this->Format,$value);
152:     }
153:     else {
154:       // These quite probably don't work.  The CSS standard for multiple classes is 'class="a b c"' but is lightly
155:       // implemented according to some web references.  Perhaps modern browsers are better?
156:       $class = $this->Align . ($this->Class == "" ? "" : " $this->Class") . ($extraclass == "" ? "" : " $extraclass");
157:       if ( $class != "" ) $class = ' class="'.$class.'"';
158:       $html = sprintf('<td%s>',$class);
159:       $html .= ($this->Format == "" ? $value : sprintf($this->Format,$value,$value));
160:       $html .= "</td>\n";
161:     }
162:     return $html;
163:   }
164: }
165: 
166: 
167: /**
168: * Start a new Browser, add columns, set a join and Render it to create a basic
169: * list of records in a table.
170: * You can, of course, get a lot fancier with setting ordering, where clauses
171: * totalled columns and so forth.
172: * @package   awl
173: */
174: class Browser
175: {
176:   var $Title;
177:   var $SubTitle;
178:   var $FieldNames;
179:   var $Columns;
180:   var $HiddenColumns;
181:   var $Joins;
182:   var $Where;
183:   var $Distinct;
184:   var $Union;
185:   var $Order;
186:   var $OrderField;
187:   var $OrderDirection;
188:   var $OrderBrowserKey;
189:   var $ForcedOrder;
190:   var $Grouping;
191:   var $Limit;
192:   var $Offset;
193:   var $Query;
194:   var $BeginRow;
195:   var $CloseRow;
196:   var $BeginRowArgs;
197:   var $BeginExtraRow;
198:   var $CloseExtraRow;
199:   var $BeginExtraRowArgs;
200:   var $Totals;
201:   var $TotalFuncs;
202:   var $ExtraRows;
203:   var $match_column;
204:   var $match_value;
205:   var $match_function;
206:   var $DivOpen;
207:   var $DivClose;
208: 
209:   /**
210:   * The Browser class constructor
211:   *
212:   * @param string $title A title for the browser (optional).
213:   */
214:   function Browser( $title = "" ) {
215:     global $c;
216:     $this->Title = $title;
217:     $this->SubTitle = "";
218:     $this->Distinct = "";
219:     $this->Order = "";
220:     $this->Limit = "";
221:     $this->Offset = "";
222:     $this->BeginRow = "<tr class=\"row%d\">\n";
223:     $this->CloseRow = "</tr>\n";
224:     $this->BeginRowArgs = array('#even');
225:     $this->Totals = array();
226:     $this->Columns = array();
227:     $this->HiddenColumns = array();
228:     $this->FieldNames = array();
229:     $this->DivOpen = '<div id="browser">';
230:     $this->DivClose = '</div>';
231:     $this->ForcedOrder = false;
232:     dbg_error_log( "Browser", ":Browser: New browser called $title");
233:   }
234: 
235:   /**
236:   * Add a column to the Browser.
237:   *
238:   * This constructs a new BrowserColumn, appending it to the array of columns
239:   * in this Browser.
240:   *
241:   * Note that if the $format parameter starts with '<td>' the format will replace
242:   * the column format, otherwise it will be used within '<td>...</td>' tags.
243:   * @see BrowserColumn
244:   *
245:   * @param string $field The name of the field.
246:   * @param string $header A column header for the field.
247:   * @param string $align An alignment for column values.
248:   * @param string $format A sprintf format for displaying column values.
249:   * @param string $sql An SQL fragment for calculating the value.
250:   * @param string $class A CSS class to apply to the cells of this column.
251:   * @param string $hook The name of a global function which will preprocess the column value
252:   *
253:   * The hook function should be defined as follows:
254:   *   function hookfunction( $column_value, $column_name, $database_row ) {
255:   *     ...
256:   *     return $value;
257:   *   }
258:   *
259:   */
260:   function AddColumn( $field, $header="", $align="", $format="", $sql="", $class="", $datatype="", $hook=null ) {
261:     $this->Columns[] = new BrowserColumn( $field, $header, $align, $format, $sql, $class, $datatype, $hook );
262:     $this->FieldNames[$field] = count($this->Columns) - 1;
263:   }
264: 
265:   /**
266:   * Add a hidden column - one that is present in the SQL result, but for
267:   * which there is no column displayed.
268:   *
269:   * This can be useful for including a value in (e.g.) clickable links or title
270:   * attributes which is not actually displayed as a visible column.
271:   *
272:   * @param string $field The name of the field.
273:   * @param string $sql An SQL fragment to calculate the field, if it is calculated.
274:   */
275:   function AddHidden( $field, $sql="" ) {
276:     $this->HiddenColumns[] = new BrowserColumn( $field, "", "", "", $sql );
277:     $this->FieldNames[$field] = count($this->Columns) - 1;
278:   }
279: 
280:   /**
281:   * Set the Title for the browse.
282:   *
283:   * This can also be set in the constructor but if you create a template Browser
284:   * and then clone it in a loop you may want to assign a different Title for each
285:   * instance.
286:   *
287:   * @param string $new_title The new title for the browser
288:   */
289:   function SetTitle( $new_title ) {
290:     $this->Title = $new_title;
291:   }
292: 
293: 
294:   /**
295:   * Accessor for the Title for the browse, which could set the title also.
296:   *
297:   * @param string $new_title The new title for the browser
298:   * @return string The current title for the browser
299:   */
300:   function Title( $new_title = null ) {
301:     if ( isset($new_title) ) $this->Title = $new_title;
302:     return $this->Title;
303:   }
304: 
305: 
306:   /**
307:   * Set the named columns to be translatable
308:   *
309:   * @param array $column_list The list of columns which are translatable
310:   */
311:   function SetTranslatable( $column_list ) {
312:     $top = count($this->Columns);
313:     for( $i=0; $i < $top; $i++ ) {
314:       dbg_error_log( "Browser", "Comparing %s with column name list", $this->Columns[$i]->Field);
315:       if ( in_array($this->Columns[$i]->Field,$column_list) ) $this->Columns[$i]->SetTranslatable();
316:     }
317:     $top = count($this->HiddenColumns);
318:     for( $i=0; $i < $top; $i++ ) {
319:       dbg_error_log( "Browser", "Comparing %s with column name list", $this->HiddenColumns[$i]->Field);
320:       if ( in_array($this->HiddenColumns[$i]->Field,$column_list) ) $this->HiddenColumns[$i]->SetTranslatable();
321:     }
322:   }
323: 
324:   /**
325:   * Set a Sub Title for the browse.
326:   *
327:   * @param string $sub_title The sub title string
328:   */
329:   function SetSubTitle( $sub_title ) {
330:     $this->SubTitle = $sub_title;
331:   }
332: 
333:   /**
334:   * Set a div for wrapping the browse.
335:   *
336:   * @param string $open_div The HTML to open the div
337:   * @param string $close_div The HTML to open the div
338:   */
339:   function SetDiv( $open_div, $close_div ) {
340:     $this->DivOpen = $open_div;
341:     $this->DivClose = $close_div;
342:   }
343: 
344:   /**
345:   * Set the tables and joins for the SQL.
346:   *
347:   * For a single table this should just contain the name of that table, but for
348:   * multiple tables it should be the full content of the SQL 'FROM ...' clause
349:   * (excluding the actual 'FROM' keyword).
350:   *
351:   * @param string $join_list
352:   */
353:   function SetJoins( $join_list ) {
354:     $this->Joins = $join_list;
355:   }
356: 
357:   /**
358:   * Set a Union SQL statement.
359:   *
360:   * In rare cases this might be useful.  It's currently a fairly simple hack
361:   * which requires you to put an entire valid (& matching) UNION subclause
362:   * (although without the UNION keyword).
363:   *
364:   * @param string $union_select
365:   */
366:   function SetUnion( $union_select ) {
367:     $this->Union = $union_select;
368:   }
369: 
370:   /**
371:   * Set the SQL Where clause to a specific value.
372:   *
373:   * The WHERE keyword should not be included.
374:   *
375:   * @param string $where_clause A valide SQL WHERE ... clause.
376:   */
377:   function SetWhere( $where_clause ) {
378:     $this->Where = $where_clause;
379:   }
380: 
381:   /**
382:   * Set the SQL DISTINCT clause to a specific value.
383:   *
384:   * The whole clause (except the keyword) needs to be supplied
385:   *
386:   * @param string $distinct The whole clause, after 'DISTINCT'
387:   */
388:   function SetDistinct( $distinct ) {
389:     $this->Distinct = "DISTINCT ".$distinct;
390:   }
391: 
392:   /**
393:   * Set the SQL LIMIT clause to a specific value.
394:   *
395:   * Only the limit number should be supplied.
396:   *
397:   * @param int $limit_n A number of rows to limit the SQL selection to
398:   */
399:   function SetLimit( $limit_n ) {
400:     $this->Limit = "LIMIT ".intval($limit_n);
401:   }
402: 
403:   /**
404:   * Set the SQL OFFSET clause to a specific value.
405:   *
406:   * Only the offset number
407:   *
408:   * @param int $offset_n A number of rows to offset the SQL selection to, based from the start of the results.
409:   */
410:   function SetOffset( $offset_n ) {
411:     $this->Offset = "OFFSET ".intval($offset_n);
412:   }
413: 
414:   /**
415:   * Add an [operator] ... to the SQL Where clause
416:   *
417:   * You will generally want to call OrWhere or AndWhere rather than
418:   * this function, but hey: who am I to tell you how to code!
419:   *
420:   * @param string $operator The operator to combine with previous where clause parts.
421:   * @param string $more_where The extra part of the where clause
422:   */
423:   function MoreWhere( $operator, $more_where ) {
424:     if ( $this->Where == "" ) {
425:       $this->Where = $more_where;
426:       return;
427:     }
428:     $this->Where = "$this->Where $operator $more_where";
429:   }
430: 
431:   /**
432:   * Add an OR ...  to the SQL Where clause
433:   *
434:   * @param string $more_where The extra part of the where clause
435:   */
436:   function AndWhere( $more_where ) {
437:     $this->MoreWhere("AND",$more_where);
438:   }
439: 
440:   /**
441:   * Add an OR ... to the SQL Where clause
442:   *
443:   * @param string $more_where The extra part of the where clause
444:   */
445:   function OrWhere( $more_where ) {
446:     $this->MoreWhere("OR",$more_where);
447:   }
448: 
449:   function AddGrouping( $field, $browser_array_key=0 ) {
450:     if ( $this->Grouping == "" )
451:       $this->Grouping = "GROUP BY ";
452:     else
453:       $this->Grouping .= ", ";
454: 
455:     $this->Grouping .= clean_string($field);
456:   }
457: 
458: 
459:   /**
460:   * Add an ordering to the browser widget.
461:   *
462:   * The ordering can be overridden by GET parameters which will be
463:   * rendered into the column headers so that a user can click on
464:   * the column headers to control the actual order.
465:   *
466:   * @param string $field The name of the field to be ordered by.
467:   * @param string $direction A for Ascending, otherwise it will be descending order.
468:   * @param string $browser_array_key Use this to distinguish between multiple
469:   *               browser widgets on the same page.  Leave it empty if you only
470:   *               have a single browser instance.
471:   * @param string $secondary Use this to indicate a default secondary order
472:   *               which shouldn't interfere with the default primary order.
473:   */
474:   function AddOrder( $field, $direction, $browser_array_key=0, $secondary=0 ) {
475:     $field = check_by_regex($field,'/^[^\'"!\\\\()\[\]|*\/{}&%@~;:?<>]+$/');
476:     if ( ! isset($this->FieldNames[$field]) ) return;
477: 
478:     if ( !isset($this->Order) || $this->Order == "" )
479:       $this->Order = "ORDER BY ";
480:     else
481:       $this->Order .= ", ";
482: 
483:     if ( $secondary == 0 ) {
484:       $this->OrderField = $field;
485:       $this->OrderBrowserKey = $browser_array_key;
486:     }
487:     $this->Order .= $field;
488: 
489:     if ( preg_match( '/^A/i', $direction) ) {
490:       $this->Order .= " ASC";
491:       if ( $secondary == 0)
492:         $this->OrderDirection = 'A';
493:     }
494:     else {
495:       $this->Order .= " DESC";
496:       if ( $secondary == 0)
497:         $this->OrderDirection = 'D';
498:     }
499:   }
500: 
501: 
502:   /**
503:   * Force a particular ordering onto the browser widget.
504:   *
505:   * @param string $field The name of the field to be ordered by.
506:   * @param string $direction A for Ascending, otherwise it will be descending order.
507:   */
508:   function ForceOrder( $field, $direction ) {
509:     $field = clean_string($field);
510:     if ( ! isset($this->FieldNames[$field]) ) return;
511: 
512:     if ( $this->Order == "" )
513:       $this->Order = "ORDER BY ";
514:     else
515:       $this->Order .= ", ";
516: 
517:     $this->Order .= $field;
518: 
519:     if ( preg_match( '/^A/i', $direction) ) {
520:       $this->Order .= " ASC";
521:     }
522:     else {
523:       $this->Order .= " DESC";
524:     }
525: 
526:     $this->ForcedOrder = true;
527:   }
528: 
529: 
530:   /**
531:   * Set up the ordering for the browser.  Generally you should call this with
532:   * the first parameter set as a field to order by default.  Call with the second
533:   * parameter set to 'D' or 'DESCEND' if you want to reverse the default order.
534:   */
535:   function SetOrdering( $default_fld=null, $default_dir='A' , $browser_array_key=0 ) {
536:     if ( isset( $_GET['o'][$browser_array_key] ) && isset($_GET['d'][$browser_array_key] ) ) {
537:       $this->AddOrder( $_GET['o'][$browser_array_key], $_GET['d'][$browser_array_key], $browser_array_key );
538:     }
539:     else {
540:       if ( ! isset($default_fld) ) $default_fld = $this->Columns[0];
541:       $this->AddOrder( $default_fld, $default_dir, $browser_array_key );
542:     }
543:   }
544: 
545: 
546:   /**
547:   * Mark a column as something to be totalled.  You can also specify the name of
548:   * a function which may modify the value before the actual totalling.
549:   *
550:   * The callback function will be called with each row, with the first argument
551:   * being the entire record object and the second argument being only the column
552:   * being totalled.  The callback should return a number, to be added to the total.
553:   *
554:   * @param string $column_name The name of the column to be totalled.
555:   * @param string $total_function The name of the callback function.
556:   */
557:   function AddTotal( $column_name, $total_function = false ) {
558:     $this->Totals[$column_name] = 0;
559:     if ( $total_function != false ) {
560:       $this->TotalFuncs[$column_name] = $total_function;
561:     }
562:   }
563: 
564: 
565:   /**
566:   * Retrieve the total from a totalled column
567:   *
568:   * @param string $column_name The name of the column to be totalled.
569:   */
570:   function GetTotal( $column_name ) {
571:     return $this->Totals[$column_name];
572:   }
573: 
574: 
575:   /**
576:   * Set the format for an output row.
577:   *
578:   * The row format is set as an sprintf format string for the start of the row,
579:   * and a plain text string for the close of the row.  Subsequent arguments
580:   * are interpreted as names of fields, the values of which will be sprintf'd
581:   * into the beginrow string for each row.
582:   *
583:   * Some special field names exist beginning with the '#' character which have
584:   * 'magic' functionality, including '#even' which will insert '0' for even
585:   * rows and '1' for odd rows, allowing a nice colour alternation if the
586:   * beginrow format refers to it like: 'class="r%d"' so that even rows will
587:   * become 'class="r0"' and odd rows will be 'class="r1"'.
588:   *
589:   * At present only '#even' exists, although other magic values may be defined
590:   * in future.
591:   *
592:   * @param string $beginrow The new printf format for the start of the row.
593:   * @param string $closerow The new string for the close of the row.
594:   * @param string $rowargs ... The row arguments which will be sprintf'd into
595:   * the $beginrow format for each row
596:   */
597:   function RowFormat( $beginrow, $closerow, $rowargs )
598:   {
599:     $argc = func_num_args();
600:     $this->BeginRow = func_get_arg(0);
601:     $this->CloseRow = func_get_arg(1);
602: 
603:     $this->BeginRowArgs = array();
604:     for( $i=2; $i < $argc; $i++ ) {
605:       $this->BeginRowArgs[] = func_get_arg($i);
606:     }
607:   }
608: 
609: 
610:   /**
611:   * Set the format for an extra output row.
612:   *
613:   * Like Browser::RowFormat(), but used for formatting additional / extra rows
614:   * created with Browser::AddRow(). Defaults to the format set with
615:   * Browser::RowFormat() if unset.
616:   *
617:   * @param string $beginrow The new printf format for the start of the extra row.
618:   * @param string $closerow The new string for the close of the extra row.
619:   * @param string $rowargs ... The row arguments which will be sprintf'd into
620:   * the $beginrow format for each extra row
621:   */
622:   function ExtraRowFormat( $beginrow, $closerow, $rowargs )
623:   {
624:     $argc = func_num_args();
625:     $this->BeginExtraRow = func_get_arg(0);
626:     $this->CloseExtraRow = func_get_arg(1);
627: 
628:     $this->BeginExtraRowArgs = array();
629:     for( $i=2; $i < $argc; $i++ ) {
630:       $this->BeginExtraRowArgs[] = func_get_arg($i);
631:     }
632:   }
633: 
634: 
635:   /**
636:   * This method is used to build and execute the database query.
637:   *
638:   * You need not call this method, since Browser::Render() will call it for
639:   * you if you have not done so at that point.
640:   *
641:   * @return boolean The success / fail status of the AwlQuery::Exec()
642:   */
643:   function DoQuery() {
644:     $target_fields = "";
645:     foreach( $this->Columns AS $k => $column ) {
646:       if ( $target_fields != "" ) $target_fields .= ", ";
647:       $target_fields .= $column->GetTarget();
648:     }
649:     if ( isset($this->HiddenColumns) ) {
650:       foreach( $this->HiddenColumns AS $k => $column ) {
651:         if ( $target_fields != "" ) $target_fields .= ", ";
652:         $target_fields .= $column->GetTarget();
653:       }
654:     }
655:     $where_clause = ((isset($this->Where) && $this->Where != "") ? "WHERE $this->Where" : "" );
656:     $sql = sprintf( "SELECT %s %s FROM %s %s %s ", $this->Distinct, $target_fields,
657:                  $this->Joins, $where_clause, $this->Grouping );
658:     if ( "$this->Union" != "" ) {
659:       $sql .= "UNION $this->Union ";
660:     }
661:     $sql .= $this->Order . ' ' . $this->Limit . ' ' . $this->Offset;
662:     $this->Query = new AwlQuery( $sql );
663:     return $this->Query->Exec("Browse:$this->Title:DoQuery");
664:   }
665: 
666: 
667:   /**
668:   * Add an extra arbitrary row onto the end of the browser.
669:   *
670:   * @var array $column_values Contains an array of named fields, hopefully matching the column names.
671:   */
672:   function AddRow( $column_values ) {
673:     if ( !isset($this->ExtraRows) || typeof($this->ExtraRows) != 'array' ) $this->ExtraRows = array();
674:     $this->ExtraRows[] = &$column_values;
675:   }
676: 
677: 
678:   /**
679:   * Replace a row where $column = $value with an extra arbitrary row, returned from calling $function
680:   *
681:   * @param string $column The name of a column to match
682:   * @param string $value  The value to match in the column
683:   * @param string $function The name of the function to call for the matched row
684:   */
685:   function MatchedRow( $column, $value, $function ) {
686:     $this->match_column = $column;
687:     $this->match_value  = $value;
688:     $this->match_function = $function;
689:   }
690: 
691: 
692:   /**
693:   * Return values from the current row for replacing into a template.
694:   *
695:   * This is used to return values from the current row, so they can
696:   * be inserted into a row template.  It is used as a callback
697:   * function for preg_replace_callback.
698:   *
699:   * @param array of string $matches An array containing a field name as offset 1
700:   */
701:   function ValueReplacement($matches)
702:   {
703:     // as usual: $matches[0] is the complete match
704:     // $matches[1] the match for the first subpattern
705:     // enclosed in '##...##' and so on
706: 
707:     $field_name = $matches[1];
708:     if ( !isset($this->current_row->{$field_name}) && substr($field_name,0,4) == "URL:" ) {
709:       $field_name = substr($field_name,4);
710:       $replacement = urlencode($this->current_row->{$field_name});
711:     }
712:     else {
713:       $replacement = (isset($this->current_row->{$field_name}) ? $this->current_row->{$field_name} : '');
714:     }
715:     dbg_error_log( "Browser", ":ValueReplacement: Replacing %s with %s", $field_name, $replacement);
716:     return $replacement;
717:   }
718: 
719: 
720:   /**
721:   * This method is used to render the browser as HTML.  If the query has
722:   * not yet been executed then this will call DoQuery to do so.
723:   *
724:   * The browser (including the title) will be displayed in a div with id="browser" so
725:   * that you can style '#browser tr.header', '#browser tr.totals' and so forth.
726:   *
727:   * @param string $title_tag The tag to use around the browser title (default 'h1')
728:   * @return string The rendered HTML fragment to display to the user.
729:   */
730:   function Render( $title_tag = null, $subtitle_tag = null ) {
731:     global $c, $BrowserCurrentRow;
732: 
733:     if ( !isset($this->Query) ) $this->DoQuery();  // Ensure the query gets run before we render!
734: 
735:     dbg_error_log( "Browser", ":Render: browser $this->Title");
736:     $html = $this->DivOpen;
737:     if ( $this->Title != "" ) {
738:       if ( !isset($title_tag) ) $title_tag = 'h1';
739:       $html .= "<$title_tag>$this->Title</$title_tag>\n";
740:     }
741:     if ( $this->SubTitle != "" ) {
742:       if ( !isset($subtitle_tag) ) $subtitle_tag = 'h2';
743:       $html .= "<$subtitle_tag>$this->SubTitle</$subtitle_tag>\n";
744:     }
745: 
746:     $html .= "<table id=\"browse_table\">\n";
747:     $html .= "<thead><tr class=\"header\">\n";
748:     foreach( $this->Columns AS $k => $column ) {
749:       $html .= $column->RenderHeader( $this->OrderField, $this->OrderDirection, $this->OrderBrowserKey, $this->ForcedOrder );
750:     }
751:     $html .= "</tr></thead>\n<tbody>";
752: 
753:     $rowanswers = array();
754:     while( $BrowserCurrentRow = $this->Query->Fetch() ) {
755: 
756:       // Work out the answers to any stuff that may be being substituted into the row start
757:       /** @TODO: We should deprecate this approach in favour of simply doing the ValueReplacement on field names */
758:       foreach( $this->BeginRowArgs AS $k => $fld ) {
759:         if ( isset($BrowserCurrentRow->{$fld}) ) {
760:           $rowanswers[$k] = $BrowserCurrentRow->{$fld};
761:         }
762:         else {
763:           switch( $fld ) {
764:             case '#even':
765:               $rowanswers[$k] = ($this->Query->rownum() % 2);
766:               break;
767:             default:
768:               $rowanswers[$k] = $fld;
769:           }
770:         }
771:       }
772:       // Start the row
773:       $row_html = vsprintf( preg_replace("/#@even@#/", ($this->Query->rownum() % 2), $this->BeginRow), $rowanswers);
774: 
775:       if ( isset($this->match_column) && isset($this->match_value) && $BrowserCurrentRow->{$this->match_column} == $this->match_value ) {
776:         $row_html .= call_user_func( $this->match_function, $BrowserCurrentRow );
777:       }
778:       else {
779:         // Each column
780:         foreach( $this->Columns AS $k => $column ) {
781:           $row_html .= $column->RenderValue( (isset($BrowserCurrentRow->{$column->Field})?$BrowserCurrentRow->{$column->Field}:'') );
782:           if ( isset($this->Totals[$column->Field]) ) {
783:             if ( isset($this->TotalFuncs[$column->Field]) && function_exists($this->TotalFuncs[$column->Field]) ) {
784:               // Run the amount through the callback function  $floatval = my_function( $row, $fieldval );
785:               $this->Totals[$column->Field] += $this->TotalFuncs[$column->Field]( $BrowserCurrentRow, $BrowserCurrentRow->{$column->Field} );
786:             }
787:             else {
788:               // Just add the amount
789:               $this->Totals[$column->Field] += doubleval( preg_replace( '/[^0-9.-]/', '', $BrowserCurrentRow->{$column->Field} ));
790:             }
791:           }
792:         }
793:       }
794: 
795:       // Finish the row
796:       $row_html .= preg_replace("/#@even@#/", ($this->Query->rownum() % 2), $this->CloseRow);
797:       $this->current_row = $BrowserCurrentRow;
798:       $html .= preg_replace_callback("/##([^#]+)##/", array( &$this, "ValueReplacement"), $row_html );
799:     }
800: 
801:     if ( count($this->Totals) > 0 ) {
802:       $BrowserCurrentRow = (object) "";
803:       $row_html = "<tr class=\"totals\">\n";
804:       foreach( $this->Columns AS $k => $column ) {
805:         if ( isset($this->Totals[$column->Field]) ) {
806:           $row_html .= $column->RenderValue( $this->Totals[$column->Field], "totals" );
807:         }
808:         else {
809:           $row_html .= $column->RenderValue( "" );
810:         }
811:       }
812:       $row_html .= "</tr>\n";
813:       $this->current_row = $BrowserCurrentRow;
814:       $html .= preg_replace_callback("/##([^#]+)##/", array( &$this, "ValueReplacement"), $row_html );
815:     }
816: 
817: 
818:     if ( count($this->ExtraRows) > 0 ) {
819:       if ( !isset($this->BeginExtraRow) )
820:         $this->BeginExtraRow = $this->BeginRow;
821:       if ( !isset($this->CloseExtraRow) )
822:         $this->CloseExtraRow = $this->CloseRow;
823:       if ( !isset($this->BeginExtraRowArgs) )
824:         $this->BeginExtraRowArgs = $this->BeginRowArgs;
825: 
826:       foreach( $this->ExtraRows AS $k => $v ) {
827:         $BrowserCurrentRow = (object) $v;
828:         // Work out the answers to any stuff that may be being substituted into the row start
829:         foreach( $this->BeginExtraRowArgs AS $k => $fld ) {
830:           if ( isset( $BrowserCurrentRow->{$fld} ) ) {
831:             $rowanswers[$k] = $BrowserCurrentRow->{$fld};
832:           }
833:           else {
834:             switch( $fld ) {
835:               case '#even':
836:                 $rowanswers[$k] = ($this->Query->rownum() % 2);
837:                 break;
838:               default:
839:                 $rowanswers[$k] = $fld;
840:             }
841:           }
842:         }
843: 
844:         // Start the row
845:         $row_html = vsprintf( preg_replace("/#@even@#/", ($this->Query->rownum() % 2), $this->BeginExtraRow), $rowanswers);
846: 
847:         if ( isset($this->match_column) && isset($this->match_value) && $BrowserCurrentRow->{$this->match_column} == $this->match_value ) {
848:           $row_html .= call_user_func( $this->match_function, $BrowserCurrentRow );
849:         }
850:         else {
851:           // Each column
852:           foreach( $this->Columns AS $k => $column ) {
853:             $row_html .= $column->RenderValue( (isset($BrowserCurrentRow->{$column->Field}) ? $BrowserCurrentRow->{$column->Field} : '') );
854:           }
855:         }
856: 
857:         // Finish the row
858:         $row_html .= preg_replace("/#@even@#/", ($this->Query->rownum() % 2), $this->CloseExtraRow);
859:         $this->current_row = $BrowserCurrentRow;
860:         $html .= preg_replace_callback("/##([^#]+)##/", array( &$this, "ValueReplacement"), $row_html );
861:       }
862:     }
863: 
864:     $html .= "</tbody>\n</table>\n";
865:     $html .= $this->DivClose;
866: 
867:     return $html;
868:   }
869: 
870: }
871: 
AWL API documentation generated by ApiGen 2.8.0