00001 <?php
00013 interface Pager {
00014 function getNavigationBar();
00015 function getBody();
00016 }
00017
00060 abstract class IndexPager implements Pager {
00061 public $mRequest;
00062 public $mLimitsShown = array( 20, 50, 100, 250, 500 );
00063 public $mDefaultLimit = 50;
00064 public $mOffset, $mLimit;
00065 public $mQueryDone = false;
00066 public $mDb;
00067 public $mPastTheEndRow;
00068
00073 protected $mIndexField;
00075 protected $mOrderType;
00087 public $mDefaultDirection;
00088 public $mIsBackwards;
00089
00093 public $mResult;
00094
00095 public function __construct() {
00096 global $wgRequest, $wgUser;
00097 $this->mRequest = $wgRequest;
00098
00099 # NB: the offset is quoted, not validated. It is treated as an
00100 # arbitrary string to support the widest variety of index types. Be
00101 # careful outputting it into HTML!
00102 $this->mOffset = $this->mRequest->getText( 'offset' );
00103
00104 # Use consistent behavior for the limit options
00105 $this->mDefaultLimit = intval( $wgUser->getOption( 'rclimit' ) );
00106 list( $this->mLimit, ) = $this->mRequest->getLimitOffset();
00107
00108 $this->mIsBackwards = ( $this->mRequest->getVal( 'dir' ) == 'prev' );
00109 $this->mDb = wfGetDB( DB_SLAVE );
00110
00111 $index = $this->getIndexField();
00112 $order = $this->mRequest->getVal( 'order' );
00113 if( is_array( $index ) && isset( $index[$order] ) ) {
00114 $this->mOrderType = $order;
00115 $this->mIndexField = $index[$order];
00116 } elseif( is_array( $index ) ) {
00117 # First element is the default
00118 reset( $index );
00119 list( $this->mOrderType, $this->mIndexField ) = each( $index );
00120 } else {
00121 # $index is not an array
00122 $this->mOrderType = null;
00123 $this->mIndexField = $index;
00124 }
00125
00126 if( !isset( $this->mDefaultDirection ) ) {
00127 $dir = $this->getDefaultDirections();
00128 $this->mDefaultDirection = is_array( $dir )
00129 ? $dir[$this->mOrderType]
00130 : $dir;
00131 }
00132 }
00133
00139 function doQuery() {
00140 # Use the child class name for profiling
00141 $fname = __METHOD__ . ' (' . get_class( $this ) . ')';
00142 wfProfileIn( $fname );
00143
00144 $descending = ( $this->mIsBackwards == $this->mDefaultDirection );
00145 # Plus an extra row so that we can tell the "next" link should be shown
00146 $queryLimit = $this->mLimit + 1;
00147
00148 $this->mResult = $this->reallyDoQuery( $this->mOffset, $queryLimit, $descending );
00149 $this->extractResultInfo( $this->mOffset, $queryLimit, $this->mResult );
00150 $this->mQueryDone = true;
00151
00152 $this->preprocessResults( $this->mResult );
00153 $this->mResult->rewind();
00154
00155 wfProfileOut( $fname );
00156 }
00157
00161 function getResult() {
00162 return $this->mResult;
00163 }
00164
00168 function setOffset( $offset ) {
00169 $this->mOffset = $offset;
00170 }
00174 function setLimit( $limit ) {
00175 $this->mLimit = $limit;
00176 }
00177
00182 function extractResultInfo( $offset, $limit, ResultWrapper $res ) {
00183 $numRows = $res->numRows();
00184 if ( $numRows ) {
00185 $row = $res->fetchRow();
00186 $firstIndex = $row[$this->mIndexField];
00187
00188 # Discard the extra result row if there is one
00189 if ( $numRows > $this->mLimit && $numRows > 1 ) {
00190 $res->seek( $numRows - 1 );
00191 $this->mPastTheEndRow = $res->fetchObject();
00192 $indexField = $this->mIndexField;
00193 $this->mPastTheEndIndex = $this->mPastTheEndRow->$indexField;
00194 $res->seek( $numRows - 2 );
00195 $row = $res->fetchRow();
00196 $lastIndex = $row[$this->mIndexField];
00197 } else {
00198 $this->mPastTheEndRow = null;
00199 # Setting indexes to an empty string means that they will be
00200 # omitted if they would otherwise appear in URLs. It just so
00201 # happens that this is the right thing to do in the standard
00202 # UI, in all the relevant cases.
00203 $this->mPastTheEndIndex = '';
00204 $res->seek( $numRows - 1 );
00205 $row = $res->fetchRow();
00206 $lastIndex = $row[$this->mIndexField];
00207 }
00208 } else {
00209 $firstIndex = '';
00210 $lastIndex = '';
00211 $this->mPastTheEndRow = null;
00212 $this->mPastTheEndIndex = '';
00213 }
00214
00215 if ( $this->mIsBackwards ) {
00216 $this->mIsFirst = ( $numRows < $limit );
00217 $this->mIsLast = ( $offset == '' );
00218 $this->mLastShown = $firstIndex;
00219 $this->mFirstShown = $lastIndex;
00220 } else {
00221 $this->mIsFirst = ( $offset == '' );
00222 $this->mIsLast = ( $numRows < $limit );
00223 $this->mLastShown = $lastIndex;
00224 $this->mFirstShown = $firstIndex;
00225 }
00226 }
00227
00237 function reallyDoQuery( $offset, $limit, $descending ) {
00238 $fname = __METHOD__ . ' (' . get_class( $this ) . ')';
00239 $info = $this->getQueryInfo();
00240 $tables = $info['tables'];
00241 $fields = $info['fields'];
00242 $conds = isset( $info['conds'] ) ? $info['conds'] : array();
00243 $options = isset( $info['options'] ) ? $info['options'] : array();
00244 $join_conds = isset( $info['join_conds'] ) ? $info['join_conds'] : array();
00245 if ( $descending ) {
00246 $options['ORDER BY'] = $this->mIndexField;
00247 $operator = '>';
00248 } else {
00249 $options['ORDER BY'] = $this->mIndexField . ' DESC';
00250 $operator = '<';
00251 }
00252 if ( $offset != '' ) {
00253 $conds[] = $this->mIndexField . $operator . $this->mDb->addQuotes( $offset );
00254 }
00255 $options['LIMIT'] = intval( $limit );
00256 $res = $this->mDb->select( $tables, $fields, $conds, $fname, $options, $join_conds );
00257 return new ResultWrapper( $this->mDb, $res );
00258 }
00259
00265 protected function preprocessResults( $result ) {}
00266
00271 function getBody() {
00272 if ( !$this->mQueryDone ) {
00273 $this->doQuery();
00274 }
00275 # Don't use any extra rows returned by the query
00276 $numRows = min( $this->mResult->numRows(), $this->mLimit );
00277
00278 $s = $this->getStartBody();
00279 if ( $numRows ) {
00280 if ( $this->mIsBackwards ) {
00281 for ( $i = $numRows - 1; $i >= 0; $i-- ) {
00282 $this->mResult->seek( $i );
00283 $row = $this->mResult->fetchObject();
00284 $s .= $this->formatRow( $row );
00285 }
00286 } else {
00287 $this->mResult->seek( 0 );
00288 for ( $i = 0; $i < $numRows; $i++ ) {
00289 $row = $this->mResult->fetchObject();
00290 $s .= $this->formatRow( $row );
00291 }
00292 }
00293 } else {
00294 $s .= $this->getEmptyBody();
00295 }
00296 $s .= $this->getEndBody();
00297 return $s;
00298 }
00299
00303 function makeLink($text, $query = null, $type=null) {
00304 if ( $query === null ) {
00305 return $text;
00306 }
00307
00308 $attrs = array();
00309 if( in_array( $type, array( 'first', 'prev', 'next', 'last' ) ) ) {
00310 # HTML5 rel attributes
00311 $attrs['rel'] = $type;
00312 }
00313
00314 if( $type ) {
00315 $attrs['class'] = "mw-{$type}link";
00316 }
00317 return $this->getSkin()->link( $this->getTitle(), $text,
00318 $attrs, $query + $this->getDefaultQuery(), array('noclasses','known') );
00319 }
00320
00325 function getStartBody() {
00326 return '';
00327 }
00328
00332 function getEndBody() {
00333 return '';
00334 }
00335
00340 function getEmptyBody() {
00341 return '';
00342 }
00343
00348 function getTitle() {
00349 return $GLOBALS['wgTitle'];
00350 }
00351
00355 function getSkin() {
00356 if ( !isset( $this->mSkin ) ) {
00357 global $wgUser;
00358 $this->mSkin = $wgUser->getSkin();
00359 }
00360 return $this->mSkin;
00361 }
00362
00368 function getDefaultQuery() {
00369 if ( !isset( $this->mDefaultQuery ) ) {
00370 $this->mDefaultQuery = $_GET;
00371 unset( $this->mDefaultQuery['title'] );
00372 unset( $this->mDefaultQuery['dir'] );
00373 unset( $this->mDefaultQuery['offset'] );
00374 unset( $this->mDefaultQuery['limit'] );
00375 unset( $this->mDefaultQuery['order'] );
00376 unset( $this->mDefaultQuery['month'] );
00377 unset( $this->mDefaultQuery['year'] );
00378 }
00379 return $this->mDefaultQuery;
00380 }
00381
00385 function getNumRows() {
00386 if ( !$this->mQueryDone ) {
00387 $this->doQuery();
00388 }
00389 return $this->mResult->numRows();
00390 }
00391
00395 function getPagingQueries() {
00396 if ( !$this->mQueryDone ) {
00397 $this->doQuery();
00398 }
00399
00400 # Don't announce the limit everywhere if it's the default
00401 $urlLimit = $this->mLimit == $this->mDefaultLimit ? '' : $this->mLimit;
00402
00403 if ( $this->mIsFirst ) {
00404 $prev = false;
00405 $first = false;
00406 } else {
00407 $prev = array( 'dir' => 'prev', 'offset' => $this->mFirstShown, 'limit' => $urlLimit );
00408 $first = array( 'limit' => $urlLimit );
00409 }
00410 if ( $this->mIsLast ) {
00411 $next = false;
00412 $last = false;
00413 } else {
00414 $next = array( 'offset' => $this->mLastShown, 'limit' => $urlLimit );
00415 $last = array( 'dir' => 'prev', 'limit' => $urlLimit );
00416 }
00417 return array( 'prev' => $prev, 'next' => $next, 'first' => $first, 'last' => $last );
00418 }
00419
00426 function getPagingLinks( $linkTexts, $disabledTexts = array() ) {
00427 $queries = $this->getPagingQueries();
00428 $links = array();
00429 foreach ( $queries as $type => $query ) {
00430 if ( $query !== false ) {
00431 $links[$type] = $this->makeLink( $linkTexts[$type], $queries[$type], $type );
00432 } elseif ( isset( $disabledTexts[$type] ) ) {
00433 $links[$type] = $disabledTexts[$type];
00434 } else {
00435 $links[$type] = $linkTexts[$type];
00436 }
00437 }
00438 return $links;
00439 }
00440
00441 function getLimitLinks() {
00442 global $wgLang;
00443 $links = array();
00444 if ( $this->mIsBackwards ) {
00445 $offset = $this->mPastTheEndIndex;
00446 } else {
00447 $offset = $this->mOffset;
00448 }
00449 foreach ( $this->mLimitsShown as $limit ) {
00450 $links[] = $this->makeLink( $wgLang->formatNum( $limit ),
00451 array( 'offset' => $offset, 'limit' => $limit ), 'num' );
00452 }
00453 return $links;
00454 }
00455
00461 abstract function formatRow( $row );
00462
00472 abstract function getQueryInfo();
00473
00484 abstract function getIndexField();
00485
00503 protected function getDefaultDirections() { return false; }
00504 }
00505
00506
00511 abstract class AlphabeticPager extends IndexPager {
00516 function getNavigationBar() {
00517 global $wgLang;
00518
00519 if( isset( $this->mNavigationBar ) ) {
00520 return $this->mNavigationBar;
00521 }
00522
00523 $opts = array( 'parsemag', 'escapenoentities' );
00524 $linkTexts = array(
00525 'prev' => wfMsgExt( 'prevn', $opts, $wgLang->formatNum( $this->mLimit ) ),
00526 'next' => wfMsgExt( 'nextn', $opts, $wgLang->formatNum($this->mLimit ) ),
00527 'first' => wfMsgExt( 'page_first', $opts ),
00528 'last' => wfMsgExt( 'page_last', $opts )
00529 );
00530
00531 $pagingLinks = $this->getPagingLinks( $linkTexts );
00532 $limitLinks = $this->getLimitLinks();
00533 $limits = $wgLang->pipeList( $limitLinks );
00534
00535 $this->mNavigationBar =
00536 "(" . $wgLang->pipeList( array( $pagingLinks['first'], $pagingLinks['last'] ) ) . ") " .
00537 wfMsgHtml( 'viewprevnext', $pagingLinks['prev'],
00538 $pagingLinks['next'], $limits );
00539
00540 if( !is_array( $this->getIndexField() ) ) {
00541 # Early return to avoid undue nesting
00542 return $this->mNavigationBar;
00543 }
00544
00545 $extra = '';
00546 $first = true;
00547 $msgs = $this->getOrderTypeMessages();
00548 foreach( array_keys( $msgs ) as $order ) {
00549 if( $first ) {
00550 $first = false;
00551 } else {
00552 $extra .= wfMsgExt( 'pipe-separator' , 'escapenoentities' );
00553 }
00554
00555 if( $order == $this->mOrderType ) {
00556 $extra .= wfMsgHTML( $msgs[$order] );
00557 } else {
00558 $extra .= $this->makeLink(
00559 wfMsgHTML( $msgs[$order] ),
00560 array( 'order' => $order )
00561 );
00562 }
00563 }
00564
00565 if( $extra !== '' ) {
00566 $this->mNavigationBar .= " ($extra)";
00567 }
00568
00569 return $this->mNavigationBar;
00570 }
00571
00579 protected function getOrderTypeMessages() {
00580 return null;
00581 }
00582 }
00583
00588 abstract class ReverseChronologicalPager extends IndexPager {
00589 public $mDefaultDirection = true;
00590 public $mYear;
00591 public $mMonth;
00592
00593 function __construct() {
00594 parent::__construct();
00595 }
00596
00597 function getNavigationBar() {
00598 global $wgLang;
00599
00600 if ( isset( $this->mNavigationBar ) ) {
00601 return $this->mNavigationBar;
00602 }
00603 $nicenumber = $wgLang->formatNum( $this->mLimit );
00604 $linkTexts = array(
00605 'prev' => wfMsgExt( 'pager-newer-n', array( 'parsemag' ), $nicenumber ),
00606 'next' => wfMsgExt( 'pager-older-n', array( 'parsemag' ), $nicenumber ),
00607 'first' => wfMsgHtml( 'histlast' ),
00608 'last' => wfMsgHtml( 'histfirst' )
00609 );
00610
00611 $pagingLinks = $this->getPagingLinks( $linkTexts );
00612 $limitLinks = $this->getLimitLinks();
00613 $limits = $wgLang->pipeList( $limitLinks );
00614
00615 $this->mNavigationBar = "({$pagingLinks['first']}" . wfMsgExt( 'pipe-separator' , 'escapenoentities' ) . "{$pagingLinks['last']}) " .
00616 wfMsgHtml("viewprevnext", $pagingLinks['prev'], $pagingLinks['next'], $limits);
00617 return $this->mNavigationBar;
00618 }
00619
00620 function getDateCond( $year, $month ) {
00621 $year = intval($year);
00622 $month = intval($month);
00623
00624 $this->mYear = $year > 0 ? $year : false;
00625 $this->mMonth = ($month > 0 && $month < 13) ? $month : false;
00626
00627
00628
00629
00630
00631 if ( !$this->mYear && !$this->mMonth ) {
00632 return;
00633 }
00634 if ( $this->mYear ) {
00635 $year = $this->mYear;
00636 } else {
00637
00638 $year = gmdate( 'Y' );
00639
00640 if( $this->mMonth > gmdate( 'n' ) ) {
00641 $year--;
00642 }
00643 }
00644 if ( $this->mMonth ) {
00645 $month = $this->mMonth + 1;
00646
00647 if ($month > 12) {
00648 $month = 1;
00649 $year++;
00650 }
00651 } else {
00652
00653 $month = 1;
00654 $year++;
00655 }
00656
00657 if ( $year > 2032 ) {
00658 $year = 2032;
00659 }
00660 $ymd = (int)sprintf( "%04d%02d01", $year, $month );
00661 if ( $ymd > 20320101 ) {
00662 $ymd = 20320101;
00663 }
00664 $this->mOffset = $this->mDb->timestamp( "${ymd}000000" );
00665 }
00666 }
00667
00672 abstract class TablePager extends IndexPager {
00673 var $mSort;
00674 var $mCurrentRow;
00675
00676 function __construct() {
00677 global $wgRequest;
00678 $this->mSort = $wgRequest->getText( 'sort' );
00679 if ( !array_key_exists( $this->mSort, $this->getFieldNames() ) ) {
00680 $this->mSort = $this->getDefaultSort();
00681 }
00682 if ( $wgRequest->getBool( 'asc' ) ) {
00683 $this->mDefaultDirection = false;
00684 } elseif ( $wgRequest->getBool( 'desc' ) ) {
00685 $this->mDefaultDirection = true;
00686 }
00687
00688 parent::__construct();
00689 }
00690
00691 function getStartBody() {
00692 global $wgStylePath;
00693 $tableClass = htmlspecialchars( $this->getTableClass() );
00694 $sortClass = htmlspecialchars( $this->getSortHeaderClass() );
00695
00696 $s = "<table border='1' class=\"$tableClass\"><thead><tr>\n";
00697 $fields = $this->getFieldNames();
00698
00699 # Make table header
00700 foreach ( $fields as $field => $name ) {
00701 if ( strval( $name ) == '' ) {
00702 $s .= "<th> </th>\n";
00703 } elseif ( $this->isFieldSortable( $field ) ) {
00704 $query = array( 'sort' => $field, 'limit' => $this->mLimit );
00705 if ( $field == $this->mSort ) {
00706 # This is the sorted column
00707 # Prepare a link that goes in the other sort order
00708 if ( $this->mDefaultDirection ) {
00709 # Descending
00710 $image = 'Arr_u.png';
00711 $query['asc'] = '1';
00712 $query['desc'] = '';
00713 $alt = htmlspecialchars( wfMsg( 'descending_abbrev' ) );
00714 } else {
00715 # Ascending
00716 $image = 'Arr_d.png';
00717 $query['asc'] = '';
00718 $query['desc'] = '1';
00719 $alt = htmlspecialchars( wfMsg( 'ascending_abbrev' ) );
00720 }
00721 $image = htmlspecialchars( "$wgStylePath/common/images/$image" );
00722 $link = $this->makeLink(
00723 "<img width=\"12\" height=\"12\" alt=\"$alt\" src=\"$image\" />" .
00724 htmlspecialchars( $name ), $query );
00725 $s .= "<th class=\"$sortClass\">$link</th>\n";
00726 } else {
00727 $s .= '<th>' . $this->makeLink( htmlspecialchars( $name ), $query ) . "</th>\n";
00728 }
00729 } else {
00730 $s .= '<th>' . htmlspecialchars( $name ) . "</th>\n";
00731 }
00732 }
00733 $s .= "</tr></thead><tbody>\n";
00734 return $s;
00735 }
00736
00737 function getEndBody() {
00738 return "</tbody></table>\n";
00739 }
00740
00741 function getEmptyBody() {
00742 $colspan = count( $this->getFieldNames() );
00743 $msgEmpty = wfMsgHtml( 'table_pager_empty' );
00744 return "<tr><td colspan=\"$colspan\">$msgEmpty</td></tr>\n";
00745 }
00746
00747 function formatRow( $row ) {
00748 $rowClass = $this->getRowClass( $row );
00749 $s = "<tr class=\"$rowClass\">\n";
00750 $fieldNames = $this->getFieldNames();
00751 $this->mCurrentRow = $row; # In case formatValue needs to know
00752 foreach ( $fieldNames as $field => $name ) {
00753 $value = isset( $row->$field ) ? $row->$field : null;
00754 $formatted = strval( $this->formatValue( $field, $value ) );
00755 if ( $formatted == '' ) {
00756 $formatted = ' ';
00757 }
00758 $class = 'TablePager_col_' . htmlspecialchars( $field );
00759 $s .= "<td class=\"$class\">$formatted</td>\n";
00760 }
00761 $s .= "</tr>\n";
00762 return $s;
00763 }
00764
00765 function getRowClass($row) {
00766 return '';
00767 }
00768
00769 function getIndexField() {
00770 return $this->mSort;
00771 }
00772
00773 function getTableClass() {
00774 return 'TablePager';
00775 }
00776
00777 function getNavClass() {
00778 return 'TablePager_nav';
00779 }
00780
00781 function getSortHeaderClass() {
00782 return 'TablePager_sort';
00783 }
00784
00788 function getNavigationBar() {
00789 global $wgStylePath, $wgContLang;
00790 $path = "$wgStylePath/common/images";
00791 $labels = array(
00792 'first' => 'table_pager_first',
00793 'prev' => 'table_pager_prev',
00794 'next' => 'table_pager_next',
00795 'last' => 'table_pager_last',
00796 );
00797 $images = array(
00798 'first' => $wgContLang->isRTL() ? 'arrow_last_25.png' : 'arrow_first_25.png',
00799 'prev' => $wgContLang->isRTL() ? 'arrow_right_25.png' : 'arrow_left_25.png',
00800 'next' => $wgContLang->isRTL() ? 'arrow_left_25.png' : 'arrow_right_25.png',
00801 'last' => $wgContLang->isRTL() ? 'arrow_first_25.png' : 'arrow_last_25.png',
00802 );
00803 $disabledImages = array(
00804 'first' => $wgContLang->isRTL() ? 'arrow_disabled_last_25.png' : 'arrow_disabled_first_25.png',
00805 'prev' => $wgContLang->isRTL() ? 'arrow_disabled_right_25.png' : 'arrow_disabled_left_25.png',
00806 'next' => $wgContLang->isRTL() ? 'arrow_disabled_left_25.png' : 'arrow_disabled_right_25.png',
00807 'last' => $wgContLang->isRTL() ? 'arrow_disabled_first_25.png' : 'arrow_disabled_last_25.png',
00808 );
00809
00810 $linkTexts = array();
00811 $disabledTexts = array();
00812 foreach ( $labels as $type => $label ) {
00813 $msgLabel = wfMsgHtml( $label );
00814 $linkTexts[$type] = "<img src=\"$path/{$images[$type]}\" alt=\"$msgLabel\"/><br/>$msgLabel";
00815 $disabledTexts[$type] = "<img src=\"$path/{$disabledImages[$type]}\" alt=\"$msgLabel\"/><br/>$msgLabel";
00816 }
00817 $links = $this->getPagingLinks( $linkTexts, $disabledTexts );
00818
00819 $navClass = htmlspecialchars( $this->getNavClass() );
00820 $s = "<table class=\"$navClass\" align=\"center\" cellpadding=\"3\"><tr>\n";
00821 $cellAttrs = 'valign="top" align="center" width="' . 100 / count( $links ) . '%"';
00822 foreach ( $labels as $type => $label ) {
00823 $s .= "<td $cellAttrs>{$links[$type]}</td>\n";
00824 }
00825 $s .= "</tr></table>\n";
00826 return $s;
00827 }
00828
00832 function getLimitSelect() {
00833 global $wgLang;
00834 $s = "<select name=\"limit\">";
00835 foreach ( $this->mLimitsShown as $limit ) {
00836 $selected = $limit == $this->mLimit ? 'selected="selected"' : '';
00837 $formattedLimit = $wgLang->formatNum( $limit );
00838 $s .= "<option value=\"$limit\" $selected>$formattedLimit</option>\n";
00839 }
00840 $s .= "</select>";
00841 return $s;
00842 }
00843
00849 function getHiddenFields( $blacklist = array() ) {
00850 $blacklist = (array)$blacklist;
00851 $query = $_GET;
00852 foreach ( $blacklist as $name ) {
00853 unset( $query[$name] );
00854 }
00855 $s = '';
00856 foreach ( $query as $name => $value ) {
00857 $encName = htmlspecialchars( $name );
00858 $encValue = htmlspecialchars( $value );
00859 $s .= "<input type=\"hidden\" name=\"$encName\" value=\"$encValue\"/>\n";
00860 }
00861 return $s;
00862 }
00863
00867 function getLimitForm() {
00868 # Make the select with some explanatory text
00869 $url = $this->getTitle()->escapeLocalURL();
00870 $msgSubmit = wfMsgHtml( 'table_pager_limit_submit' );
00871 return
00872 "<form method=\"get\" action=\"$url\">" .
00873 wfMsgHtml( 'table_pager_limit', $this->getLimitSelect() ) .
00874 "\n<input type=\"submit\" value=\"$msgSubmit\"/>\n" .
00875 $this->getHiddenFields( array('limit','title') ) .
00876 "</form>\n";
00877 }
00878
00885 abstract function isFieldSortable( $field );
00886
00897 abstract function formatValue( $name, $value );
00898
00902 abstract function getDefaultSort();
00903
00909 abstract function getFieldNames();
00910 }