Version: current

       1 <?php
       2
       3 /**
       4  * @package forms
       5  * @subpackage fields-relational
       6  */
       7
       8 /**
       9  * Form field that embeds a list into a form, such as a member list or a file list.
      10  *
      11  * All get variables are namespaced in the format ctf[MyFieldName][MyParameter] to avoid collisions
      12  * when multiple TableListFields are present in a form.
      13  *
      14  * @param $name string The fieldname
      15  * @param $sourceClass string The source class of this field
      16  * @param $fieldList array An array of field headings of Fieldname => Heading Text (eg. heading1)
      17  * @param $sourceFilter string The filter field you wish to limit the objects by (eg. parentID)
      18  * @param $sourceSort string
      19  * @param $sourceJoin string
      20  * @package forms
      21  * @subpackage fields-relational
      22  */
      23 class TableListField extends FormField {
      24
      25     /**
      26      * @var $cachedSourceItems DataObjectSet Prevent {@sourceItems()} from being called multiple times.
      27      */
      28     protected $cachedSourceItems;
      29
      30     protected $sourceClass;
      31
      32     protected $sourceFilter = "";
      33
      34     protected $sourceSort = "";
      35
      36     protected $sourceJoin = array();
      37
      38     protected $fieldList;
      39
      40     protected $disableSorting = false;
      41
      42     /**
      43      * @var $fieldListCsv array
      44      */
      45     protected $fieldListCsv;
      46
      47     /**
      48      * @var $clickAction
      49      */
      50     protected $clickAction;
      51
      52     /**
      53      * @var bool
      54      */
      55     public $IsReadOnly;
      56
      57     /**
      58      * Called method (needs to be retained for AddMode())
      59      */
      60     protected $methodName;
      61
      62     /**
      63      * @var $summaryFieldList array Shows a row which summarizes the contents of a column by a predefined
      64      * Javascript-function
      65      */
      66     protected $summaryFieldList;
      67
      68     /**
      69      * @var $summaryTitle string The title which will be shown in the first column of the summary-row.
      70      * Accordingly, the first column can't be used for summarizing.
      71      */
      72     protected $summaryTitle;
      73
      74     /**
      75      * @var $template string Template-Overrides
      76      */
      77     protected $template = "TableListField";
      78
      79     /**
      80      * @var $itemClass string Class name for each item/row
      81      */
      82     public $itemClass = 'TableListField_Item';
      83
      84     /**
      85      * @var bool Do we use checkboxes to mark records, or delete them one by one?
      86      */
      87     public $Markable;
      88
      89     public $MarkableTitle = null;
      90
      91     /**
      92      * @var $readOnly boolean Deprecated, please use $permssions instead
      93      */
      94     protected $readOnly;
      95
      96     /**
      97      * @var $permissions array Influence output without having to subclass the template.
      98      * See $actions for adding your custom actions/permissions.
      99      */
     100     protected $permissions = array(
     101         //"print",
     102         //"export",
     103         "delete"
     104     );
     105
     106     /**
     107      * @var $actions array Action that can be performed on a single row-entry.
     108      * Has to correspond to a method in a TableListField-class (or subclass).
     109      * Actions can be disabled through $permissions.
     110      * Format (key is used for the methodname and CSS-class):
     111      * array(
     112      *  'delete' => array(
     113      *      'label' => 'Delete',
     114      *      'icon' => 'cms/images/delete.gif',
     115      *      'icon_disabled' => 'cms/images/delete_disabled.gif',
     116      *      'class' => 'deletelink',
     117      *  )
     118      * )
     119      */
     120     public $actions = array(
     121         'delete' => array(
     122             'label' => 'Delete',
     123             'icon' => 'cms/images/delete.gif',
     124             'icon_disabled' => 'cms/images/delete_disabled.gif',
     125             'class' => 'deletelink'
     126         )
     127     );
     128
     129     /**
     130      * @var $defaultAction String Action being executed when clicking on table-row (defaults to "show").
     131      * Mostly needed in ComplexTableField-subclass.
     132      */
     133     public $defaultAction = '';
     134
     135     /**
     136      * @var $customQuery Specify custom query, e.g. for complicated having/groupby-constructs.
     137      * Caution: TableListField automatically selects the ID from the {@sourceClass}, because it relies
     138      * on this information e.g. in saving a TableField. Please use a custom select if you want to filter
     139      * for other IDs in joined tables: $query->select[] = "MyJoinedTable.ID AS MyJoinedTableID"
     140      */
     141     protected $customQuery;
     142
     143     /**
     144      * @var $customCsvQuery Query for CSV-export (might need different fields or further filtering)
     145      */
     146     protected $customCsvQuery;
     147
     148     /**
     149      * @var $customSourceItems DataObjectSet Use the manual setting of a result-set only as a last-resort
     150      * for sets which can't be resolved in a single query.
     151      *
     152      * @todo Add pagination support for customSourceItems.
     153      */
     154     protected $customSourceItems;
     155
     156     /**
     157      * Character to seperate exported columns in the CSV file
     158      */
     159     protected $csvSeparator = ";";
     160
     161     /*
     162      * Boolean deciding whether to include a header row in the CSV file
     163      */
     164     protected $csvHasHeader = true;
     165
     166     /**
     167      * @var array Specify custom escape for the fields.
     168      *
     169      * <code>
     170      * array("\""=>"\"\"","\r"=>"", "\r\n"=>"", "\n"=>"")
     171      * </code>
     172      */
     173     public $csvFieldEscape = array(
     174         "\""=>"\"\"",
     175         "\r\n"=>"",
     176         "\r"=>"",
     177         "\n"=>"",
     178     );
     179
     180
     181     /**
     182      * @var int Shows total count regardless or pagination
     183      */
     184     protected $totalCount;
     185
     186     /**
     187      * @var boolean Trigger pagination
     188      */
     189     protected $showPagination = false;
     190
     191     /**
     192      * @var string Override the {@link Link()} method
     193      * for all pagination. Useful to force rendering of the field
     194      * in a different context.
     195      */
     196     public $paginationBaseLink = null;
     197
     198     /**
     199      * @var int Number of items to show on a single page (needed for pagination)
     200      */
     201     protected $pageSize = 10;
     202
     203     /**
     204      * @var array Definitions for highlighting table-rows with a specific class. You can use all column-names
     205      * in the result of a query. Use in combination with {@setCustomQuery} to select custom properties and joined objects.
     206      *
     207      * Example:
     208      * array(
     209      *  array(
     210      *      "rule" => '$Flag == "red"',
     211      *      "class" => "red"
     212      *  ),
     213      *  array(
     214      *      "rule" => '$Flag == "orange"',
     215      *      "class" => "orange"
     216      *  )
     217      * )
     218      */
     219     public $highlightConditions = array();
     220
     221     /**
     222      * @var array Specify castings with fieldname as the key, and the desired casting as value.
     223      * Example: array("MyCustomDate"=>"Date","MyShortText"=>"Text->FirstSentence")
     224      */
     225     public $fieldCasting = array();
     226
     227     /**
     228      * @var array Specify custom formatting for fields, e.g. to render a link instead of pure text.
     229      * Caution: Make sure to escape special php-characters like in a normal php-statement.
     230      * Example: "myFieldName" => '<a href=\"custom-admin/$ID\">$ID</a>'
     231      */
     232     public $fieldFormatting = array();
     233
     234     public $csvFieldFormatting = array();
     235
     236     /**
     237      * @var string
     238      */
     239     public $exportButtonLabel = 'Export as CSV';
     240
     241     /**
     242      * @var string $groupByField Used to group by a specific column in the DataObject
     243      * and create partial summaries.
     244      */
     245     public $groupByField = null;
     246
     247     /**
     248      * @var array
     249      */
     250     protected $extraLinkParams;
     251
     252     protected $__cachedQuery;
     253
     254     function __construct($name, $sourceClass, $fieldList = null, $sourceFilter = null,
     255         $sourceSort = null, $sourceJoin = null) {
     256
     257         $this->fieldList = ($fieldList) ? $fieldList : singleton($sourceClass)->summaryFields();
     258         $this->sourceClass = $sourceClass;
     259         $this->sourceFilter = $sourceFilter;
     260         $this->sourceSort = $sourceSort;
     261         $this->sourceJoin = $sourceJoin;
     262         $this->readOnly = false;
     263
     264         parent::__construct($name);
     265     }
     266
     267     /**
     268      * Get the filter
     269      */
     270     function sourceFilter() {
     271         return $this->sourceFilter;
     272     }
     273
     274     function index() {
     275         return $this->FieldHolder();
     276     }
     277
     278     static $url_handlers = array(
     279         'item/$ID' => 'handleItem',
     280         '$Action' => '$Action',
     281     );
     282
     283     function sourceClass() {
     284         return $this->sourceClass;
     285     }
     286
     287     function handleItem($request) {
     288         return new TableListField_ItemRequest($this, $request->param('ID'));
     289     }
     290
     291     function FieldHolder() {
     292         Requirements::javascript(SAPPHIRE_DIR . '/thirdparty/prototype/prototype.js');
     293         Requirements::javascript(SAPPHIRE_DIR . '/thirdparty/behaviour/behaviour.js');
     294         Requirements::javascript(SAPPHIRE_DIR . '/javascript/prototype_improvements.js');
     295         Requirements::javascript(THIRDPARTY_DIR . '/scriptaculous/effects.js');
     296         Requirements::add_i18n_javascript(SAPPHIRE_DIR . '/javascript/lang');
     297         Requirements::javascript(SAPPHIRE_DIR . '/javascript/TableListField.js');
     298         Requirements::css(SAPPHIRE_DIR . '/css/TableListField.css');
     299
     300         if($this->clickAction) {
     301             $id = $this->id();
     302             Requirements::customScript(<<<JS
     303                 Behaviour.register({
     304                     '#$id tr' : {
     305                         onclick : function() {
     306                             $this->clickAction
     307                             return false;
     308                         }
     309                 }
     310             });
     311 JS
     312         );}
     313         return $this->renderWith($this->template);
     314     }
     315
     316     function Headings() {
     317         $headings = array();
     318         foreach($this->fieldList as $fieldName => $fieldTitle) {
     319             $isSorted = (isset($_REQUEST['ctf'][$this->Name()]['sort']) && $fieldName == $_REQUEST['ctf'][$this->Name()]['sort']);
     320             // we can't allow sorting with partial summaries (groupByField)
     321             $isSortable = ($this->form && $this->isFieldSortable($fieldName) && !$this->groupByField);
     322
     323             // sorting links (only if we have a form to refresh with)
     324             if($this->form) {
     325                 $sortLink = $this->Link();
     326                 $sortLink = HTTP::setGetVar("ctf[{$this->Name()}][sort]", $fieldName, $sortLink,'&');
     327
     328                 // Apply sort direction to the current sort field
     329                 if(!empty($_REQUEST['ctf'][$this->Name()]['sort']) && ($_REQUEST['ctf'][$this->Name()]['sort'] == $fieldName)) {
     330                     $dir = isset($_REQUEST['ctf'][$this->Name()]['dir']) ? $_REQUEST['ctf'][$this->Name()]['dir'] : null;
     331                     $dir = trim(strtolower($dir));
     332                     $newDir = ($dir == 'desc') ? null : 'desc';
     333                     $sortLink = HTTP::setGetVar("ctf[{$this->Name()}][dir]", Convert::raw2xml($newDir), $sortLink,'&');
     334                 }
     335
     336                 if(isset($_REQUEST['ctf'][$this->Name()]['search']) && is_array($_REQUEST['ctf'][$this->Name()]['search'])) {
     337                     foreach($_REQUEST['ctf'][$this->Name()]['search'] as $parameter => $value) {
     338                         $XML_search = Convert::raw2xml($value);
     339                         $sortLink = HTTP::setGetVar("ctf[{$this->Name()}][search][$parameter]", $XML_search, $sortLink,'&');
     340                     }
     341                 }
     342             } else {
     343                 $sortLink = '#';
     344             }
     345
     346             $headings[] = new ArrayData(array(
     347                 "Name" => $fieldName,
     348                 "Title" => ($this->sourceClass) ? singleton($this->sourceClass)->fieldLabel($fieldTitle) : $fieldTitle,
     349                 "IsSortable" => $isSortable,
     350                 "SortLink" => $sortLink,
     351                 "SortBy" => $isSorted,
     352                 "SortDirection" => (isset($_REQUEST['ctf'][$this->Name()]['dir'])) ? $_REQUEST['ctf'][$this->Name()]['dir'] : null
     353             ));
     354         }
     355         return new DataObjectSet($headings);
     356     }
     357
     358     function disableSorting($to = true) {
     359         $this->disableSorting = $to;
     360     }
     361
     362     /**
     363      * Determines if a field is "sortable".
     364      * If the field is generated by a custom getter, we can't sort on it
     365      * without generating all objects first (which would be a huge performance impact).
     366      *
     367      * @param string $fieldName
     368      * @return bool
     369      */
     370     function isFieldSortable($fieldName) {
     371         if($this->customSourceItems || $this->disableSorting) {
     372             return false;
     373         }
     374
     375         if(!$this->__cachedQuery) $this->__cachedQuery = $this->getQuery();
     376
     377         return $this->__cachedQuery->canSortBy($fieldName);
     378     }
     379
     380     /**
     381      * Dummy function to get number of actions originally generated in
     382      * TableListField_Item.
     383      *
     384      * @return DataObjectSet
     385      */
     386     function Actions() {
     387         $allowedActions = new DataObjectSet();
     388         foreach($this->actions as $actionName => $actionSettings) {
     389             if($this->Can($actionName)) {
     390                 $allowedActions->push(new ViewableData());
     391             }
     392         }
     393
     394         return $allowedActions;
     395     }
     396
     397     /**
     398      * Provide a custom query to compute sourceItems. This is the preferred way to using
     399      * {@setSourceItems}, because we can still paginate.
     400      * Caution: Other parameters such as {@sourceFilter} will be ignored.
     401      * Please use this only as a fallback for really complex queries (e.g. involving HAVING and GROUPBY).
     402      *
     403      * @param $query SS_Query
     404      */
     405     function setCustomQuery(SQLQuery $query) {
     406         // The type-hinting above doesn't seem to work consistently
     407         if($query instanceof SQLQuery) {
     408             $this->customQuery = $query;
     409         } else {
     410             user_error('TableList::setCustomQuery() should be passed a SQLQuery', E_USER_WARNING);
     411         }
     412     }
     413
     414     function setCustomCsvQuery(SQLQuery $query) {
     415         // The type-hinting above doesn't seem to work consistently
     416         if($query instanceof SQLQuery) {
     417             $this->customCsvQuery = $query;
     418         } else {
     419             user_error('TableList::setCustomCsvQuery() should be passed a SQLQuery', E_USER_WARNING);
     420         }
     421     }
     422
     423     function setCustomSourceItems(DataObjectSet $items) {
     424         // The type-hinting above doesn't seem to work consistently
     425         if($items instanceof DataObjectSet) {
     426             $this->customSourceItems = $items;
     427         } else {
     428             user_error('TableList::setCustomSourceItems() should be passed a DataObjectSet', E_USER_WARNING);
     429         }
     430     }
     431
     432     function sourceItems() {
     433         $SQL_limit = ($this->showPagination && $this->pageSize) ? "{$this->pageSize}" : null;
     434         if(isset($_REQUEST['ctf'][$this->Name()]['start']) && is_numeric($_REQUEST['ctf'][$this->Name()]['start'])) {
     435             $SQL_start = (isset($_REQUEST['ctf'][$this->Name()]['start'])) ? intval($_REQUEST['ctf'][$this->Name()]['start']) : "0";
     436         } else {
     437             $SQL_start = 0;
     438         }
     439         if(isset($this->customSourceItems)) {
     440             if($this->showPagination && $this->pageSize) {
     441                 $items = $this->customSourceItems->getRange($SQL_start, $SQL_limit);
     442             } else {
     443                 $items = $this->customSourceItems;
     444             }
     445         } elseif(isset($this->cachedSourceItems)) {
     446             $items = $this->cachedSourceItems;
     447         } else {
     448             // get query
     449             $dataQuery = $this->getQuery();
     450
     451             // we don't limit when doing certain actions        T
     452             $methodName = isset($_REQUEST['url']) ? array_pop(explode('/', $_REQUEST['url'])) : null;
     453             if(!$methodName || !in_array($methodName,array('printall','export'))) {
     454                 $dataQuery->limit(array(
     455                     'limit' => $SQL_limit,
     456                     'start' => (isset($SQL_start)) ? $SQL_start : null
     457                 ));
     458             }
     459
     460             // get data
     461             $records = $dataQuery->execute();
     462             $sourceClass = $this->sourceClass;
     463             $dataobject = new $sourceClass();
     464             $items = $dataobject->buildDataObjectSet($records, 'DataObjectSet');
     465
     466             $this->cachedSourceItems = $items;
     467         }
     468
     469         return $items;
     470     }
     471
     472     function Items() {
     473         $fieldItems = new DataObjectSet();
     474         if($items = $this->sourceItems()) foreach($items as $item) {
     475             if($item) $fieldItems->push(new $this->itemClass($item, $this));
     476         }
     477         return $fieldItems;
     478     }
     479
     480     /**
     481      * Generates the query for sourceitems (without pagination/limit-clause)
     482      *
     483      * @return string
     484      */
     485     function getQuery() {
     486         if($this->customQuery) {
     487             $query = clone $this->customQuery;
     488             $baseClass = ClassInfo::baseDataClass($this->sourceClass);
     489         } else {
     490             $query = singleton($this->sourceClass)->extendedSQL($this->sourceFilter(), $this->sourceSort, null, $this->sourceJoin);
     491         }
     492
     493         if(!empty($_REQUEST['ctf'][$this->Name()]['sort'])) {
     494             $column = $_REQUEST['ctf'][$this->Name()]['sort'];
     495             $dir = 'ASC';
     496             if(!empty($_REQUEST['ctf'][$this->Name()]['dir'])) {
     497                 $dir = $_REQUEST['ctf'][$this->Name()]['dir'];
     498                 if(strtoupper(trim($dir)) == 'DESC') $dir = 'DESC';
     499             }
     500             if(!empty($_REQUEST['ctf'][$this->Name()]['sort_dir'])) {
     501                 $dir = $_REQUEST['ctf'][$this->Name()]['sort_dir'];
     502                 if(strtoupper(trim($dir)) == 'DESC') $dir = 'DESC';
     503             }
     504             if($query->canSortBy($column)) $query->orderby = $column.' '.$dir;
     505         }
     506
     507         return $query;
     508     }
     509
     510     function getCsvQuery() {
     511         $baseClass = ClassInfo::baseDataClass($this->sourceClass);
     512         if($this->customCsvQuery || $this->customQuery) {
     513             $query = $this->customCsvQuery ? $this->customCsvQuery : $this->customQuery;
     514         } else {
     515             $query = singleton($this->sourceClass)->extendedSQL($this->sourceFilter(), $this->sourceSort, null, $this->sourceJoin);
     516         }
     517
     518         return clone $query;
     519     }
     520
     521     function FieldList() {
     522         return $this->fieldList;
     523     }
     524
     525     /**
     526      * Configure this table to load content into a subform via ajax
     527      */
     528     function setClick_AjaxLoad($urlBase, $formID) {
     529         $this->clickAction = "this.ajaxRequest('" . addslashes($urlBase) . "', '" . addslashes($formID) . "')";
     530     }
     531
     532     /**
     533      * Configure this table to open a popup window
     534      */
     535     function setClick_PopupLoad($urlBase) {
     536         $this->clickAction = "var w = window.open(baseHref() + '$urlBase' + this.id.replace(/.*-(\d*)$/,'$1'), 'popup'); w.focus();";
     537     }
     538
     539     function performReadonlyTransformation() {
     540         $clone = clone $this;
     541         $clone->setShowPagination(false);
     542
     543         // Only include the show action if it was in the original CTF.
     544         $clone->setPermissions(in_array('show', $this->permissions) ? array('show') : array());
     545
     546         $clone->addExtraClass( 'readonly' );
     547         $clone->setReadonly(true);
     548         return $clone;
     549     }
     550
     551     /**
     552      * #################################
     553      *           CRUD
     554      * #################################
     555      */
     556
     557     /**
     558      * @return String
     559      */
     560     function delete() {
     561         if($this->Can('delete') !== true) {
     562             return false;
     563         }
     564
     565         $this->methodName = "delete";
     566
     567         $childId = Convert::raw2sql($_REQUEST['ctf']['childID']);
     568
     569         if (is_numeric($childId)) {
     570             $childObject = DataObject::get_by_id($this->sourceClass, $childId);
     571             if($childObject) $childObject->delete();
     572         }
     573
     574         // TODO return status in JSON etc.
     575         //return $this->renderWith($this->template);
     576     }
     577
     578
     579     /**
     580      * #################################
     581      *           Summary-Row
     582      * #################################
     583      */
     584
     585     /**
     586      * Can utilize some built-in summary-functions, with optional casting.
     587      * Currently supported:
     588      * - sum
     589      * - avg
     590      *
     591      * @param $summaryTitle string
     592      * @param $summaryFields array
     593      * Simple Format: array("MyFieldName"=>"sum")
     594      * With Casting: array("MyFieldname"=>array("sum","Currency->Nice"))
     595      */
     596     function addSummary($summaryTitle, $summaryFieldList) {
     597         $this->summaryTitle = $summaryTitle;
     598         $this->summaryFieldList = $summaryFieldList;
     599     }
     600
     601     function removeSummary() {
     602         $this->summaryTitle = null;
     603         $this->summaryFields = null;
     604     }
     605
     606     function HasSummary() {
     607         return (isset($this->summaryFieldList));
     608     }
     609
     610     function SummaryTitle() {
     611         return $this->summaryTitle;
     612     }
     613
     614     /**
     615      * @param DataObjectSet $items Only used to pass grouped sourceItems for creating
     616      * partial summaries.
     617      */
     618     function SummaryFields($items = null) {
     619         if(!isset($this->summaryFieldList)) {
     620             return false;
     621         }
     622         $summaryFields = array();
     623         $fieldListWithoutFirst = $this->fieldList;
     624         if(!empty($this->summaryTitle)) {
     625             array_shift($fieldListWithoutFirst);
     626         }
     627         foreach($fieldListWithoutFirst as $fieldName => $fieldTitle) {
     628
     629             if(in_array($fieldName, array_keys($this->summaryFieldList))) {
     630                 if(is_array($this->summaryFieldList[$fieldName])) {
     631                     $summaryFunction = "colFunction_{$this->summaryFieldList[$fieldName][0]}";
     632                     $casting = $this->summaryFieldList[$fieldName][1];
     633                 } else {
     634                     $summaryFunction = "colFunction_{$this->summaryFieldList[$fieldName]}";
     635                     $casting = null;
     636                 }
     637
     638                 // fall back to integrated sourceitems if not passed
     639                 if(!$items) $items = $this->sourceItems();
     640
     641                 $summaryValue = ($items) ? $this->$summaryFunction($items->column($fieldName)) : null;
     642
     643                 // Optional casting, Format: array('MyFieldName'=>array('sum','Currency->Nice'))
     644                 if(isset($casting)) {
     645                     $summaryValue = $this->getCastedValue($summaryValue, $casting);
     646                 }
     647             } else {
     648                 $summaryValue = null;
     649                 $function = null;
     650             }
     651
     652             $summaryFields[] = new ArrayData(array(
     653                 'Function' => $function,
     654                 'SummaryValue' => $summaryValue,
     655                 'Name' => DBField::create('Varchar', $fieldName),
     656                 'Title' => DBField::create('Varchar', $fieldTitle),
     657             ));
     658         }
     659         return new DataObjectSet($summaryFields);
     660     }
     661
     662     function HasGroupedItems() {
     663         return ($this->groupByField);
     664     }
     665
     666     function GroupedItems() {
     667         if(!$this->groupByField) {
     668             return false;
     669         }
     670
     671         $items = $this->sourceItems();
     672         if(!$items || !$items->Count()) {
     673             return false;
     674         }
     675
     676         $groupedItems = $items->groupBy($this->groupByField);
     677         $groupedArrItems = new DataObjectSet();
     678         foreach($groupedItems as $key => $group) {
     679             $fieldItems = new DataObjectSet();
     680             foreach($group as $item) {
     681                 if($item) $fieldItems->push(new $this->itemClass($item, $this));
     682             }
     683             $groupedArrItems->push(new ArrayData(array(
     684                 'Items' => $fieldItems,
     685                 'SummaryFields' => $this->SummaryFields($group)
     686             )));
     687         }
     688
     689         return $groupedArrItems;
     690     }
     691
     692     function colFunction_sum($values) {
     693         return array_sum($values);
     694     }
     695
     696     function colFunction_avg($values) {
     697         return array_sum($values)/count($values);
     698     }
     699
     700
     701     /**
     702      * #################################
     703      *           Permissions
     704      * #################################
     705      */
     706
     707     /**
     708      * Template accessor for Permissions.
     709      * See {@link TableListField_Item->Can()} for object-specific
     710      * permissions.
     711      *
     712      * @return boolean
     713      */
     714     function Can($mode) {
     715         if($mode == 'add' && $this->isReadonly()) {
     716             return false;
     717         } else if($mode == 'delete' && $this->isReadonly()) {
     718             return false;
     719         } else if($mode == 'edit' && $this->isReadonly()) {
     720             return false;
     721         } else {
     722             return (in_array($mode, $this->permissions));
     723         }
     724
     725     }
     726
     727     function setPermissions($arr) {
     728         $this->permissions = $arr;
     729     }
     730
     731     /**
     732      * @return array
     733      */
     734     function getPermissions() {
     735         return $this->permissions;
     736     }
     737
     738     /**
     739      * #################################
     740      *           Pagination
     741      * #################################
     742      */
     743     function setShowPagination($bool) {
     744         $this->showPagination = (bool)$bool;
     745     }
     746
     747     /**
     748      * @return boolean
     749      */
     750     function ShowPagination() {
     751         if($this->showPagination && !empty($this->summaryFieldList)) {
     752             user_error("You can't combine pagination and summaries - please disable one of them.", E_USER_ERROR);
     753         }
     754         return $this->showPagination;
     755     }
     756
     757     function setPageSize($pageSize) {
     758         $this->pageSize = $pageSize;
     759     }
     760
     761      function PageSize() {
     762         return $this->pageSize;
     763     }
     764
     765     function ListStart() {
     766         return $_REQUEST['ctf'][$this->Name()]['start'];
     767     }
     768
     769     /**
     770      * @param array
     771      * @deprecated Put the query string onto your form's link instead :-)
     772      */
     773     function setExtraLinkParams($params){
     774         user_error("TableListField::setExtraLinkParams() deprecated - put the query string onto your form's FormAction instead; it will be handed down to all field with special handlers", E_USER_NOTICE);
     775         $this->extraLinkParams = $params;
     776     }
     777
     778     /**
     779      * @return array
     780      */
     781     function getExtraLinkParams(){
     782         return $this->extraLinkParams;
     783     }
     784
     785     function FirstLink() {
     786         $start = 0;
     787
     788         if(!isset($_REQUEST['ctf'][$this->Name()]['start']) || !is_numeric($_REQUEST['ctf'][$this->Name()]['start']) || $_REQUEST['ctf'][$this->Name()]['start'] == 0) {
     789             return null;
     790         }
     791         $baseLink = ($this->paginationBaseLink) ? $this->paginationBaseLink : $this->Link();
     792         $link = Controller::join_links($baseLink, "?ctf[{$this->Name()}][start]={$start}");
     793         if(isset($_REQUEST['ctf'][$this->Name()]['sort']) && ($sort = $_REQUEST['ctf'][$this->Name()]['sort'])) {
     794             $sortString = "ctf[{$this->Name()}][sort]={$sort}";
     795             if(isset($_REQUEST['ctf'][$this->Name()]['dir']) && ($dir = $_REQUEST['ctf'][$this->Name()]['dir'])) {
     796                 $sortString .= "&ctf[{$this->Name()}][dir]={$dir}";
     797             }
     798             $link .= "&" .$sortString;
     799         }
     800         if($this->extraLinkParams) $link .= "&" . http_build_query($this->extraLinkParams);
     801         return $link;
     802     }
     803
     804     function PrevLink() {
     805         $currentStart = isset($_REQUEST['ctf'][$this->Name()]['start']) ? $_REQUEST['ctf'][$this->Name()]['start'] : 0;
     806
     807         if($currentStart == 0) {
     808             return null;
     809         }
     810
     811         $start = ($_REQUEST['ctf'][$this->Name()]['start'] - $this->pageSize < 0)  ? 0 : $_REQUEST['ctf'][$this->Name()]['start'] - $this->pageSize;
     812
     813         $baseLink = ($this->paginationBaseLink) ? $this->paginationBaseLink : $this->Link();
     814         $link = Controller::join_links($baseLink, "?ctf[{$this->Name()}][start]={$start}");
     815         if(isset($_REQUEST['ctf'][$this->Name()]['sort']) && ($sort = $_REQUEST['ctf'][$this->Name()]['sort'])) {
     816             $sortString = "ctf[{$this->Name()}][sort]={$sort}";
     817             if(isset($_REQUEST['ctf'][$this->Name()]['dir']) && ($dir = $_REQUEST['ctf'][$this->Name()]['dir'])) {
     818                 $sortString .= "&ctf[{$this->Name()}][dir]={$dir}";
     819             }
     820             $link .= "&" .$sortString;
     821         }
     822         if($this->extraLinkParams) $link .= "&" . http_build_query($this->extraLinkParams);
     823         return $link;
     824     }
     825
     826
     827     function NextLink() {
     828         $currentStart = isset($_REQUEST['ctf'][$this->Name()]['start']) ? $_REQUEST['ctf'][$this->Name()]['start'] : 0;
     829         $start = ($currentStart + $this->pageSize < $this->TotalCount()) ? $currentStart + $this->pageSize : $this->TotalCount() % $this->pageSize > 0;
     830         if($currentStart >= $start-1) {
     831             return null;
     832         }
     833         $baseLink = ($this->paginationBaseLink) ? $this->paginationBaseLink : $this->Link();
     834         $link = Controller::join_links($baseLink, "?ctf[{$this->Name()}][start]={$start}");
     835         if(isset($_REQUEST['ctf'][$this->Name()]['sort']) && ($sort = $_REQUEST['ctf'][$this->Name()]['sort'])) {
     836             $sortString = "ctf[{$this->Name()}][sort]={$sort}";
     837             if(isset($_REQUEST['ctf'][$this->Name()]['dir']) && ($dir = $_REQUEST['ctf'][$this->Name()]['dir'])) {
     838                 $sortString .= "&ctf[{$this->Name()}][dir]={$dir}";
     839             }
     840             $link .= "&" .$sortString;
     841         }
     842         if($this->extraLinkParams) $link .= "&" . http_build_query($this->extraLinkParams);
     843         return $link;
     844     }
     845
     846     function LastLink() {
     847         $pageSize = ($this->TotalCount() % $this->pageSize > 0) ? $this->TotalCount() % $this->pageSize : $this->pageSize;
     848         $start = $this->TotalCount() - $pageSize;
     849         // Check if there is only one page, or if we are on last page
     850         if($this->TotalCount() <= $pageSize || (isset($_REQUEST['ctf'][$this->Name()]['start']) &&  $_REQUEST['ctf'][$this->Name()]['start'] >= $start)) {
     851             return null;
     852         }
     853
     854         $baseLink = ($this->paginationBaseLink) ? $this->paginationBaseLink : $this->Link();
     855         $link = Controller::join_links($baseLink, "?ctf[{$this->Name()}][start]={$start}");
     856         if(isset($_REQUEST['ctf'][$this->Name()]['sort']) && ($sort = $_REQUEST['ctf'][$this->Name()]['sort'])) {
     857             $sortString = "ctf[{$this->Name()}][sort]={$sort}";
     858             if(isset($_REQUEST['ctf'][$this->Name()]['dir']) && ($dir = $_REQUEST['ctf'][$this->Name()]['dir'])) {
     859                 $sortString .= "&ctf[{$this->Name()}][dir]={$dir}";
     860             }
     861             $link .= "&" .$sortString;
     862         }
     863         if($this->extraLinkParams) $link .= "&" . http_build_query($this->extraLinkParams);
     864         return $link;
     865     }
     866
     867     function FirstItem() {
     868         if ($this->TotalCount() < 1) return 0;
     869         return isset($_REQUEST['ctf'][$this->Name()]['start']) ? $_REQUEST['ctf'][$this->Name()]['start'] + 1 : 1;
     870     }
     871
     872     function LastItem() {
     873         if(isset($_REQUEST['ctf'][$this->Name()]['start'])) {
     874             return $_REQUEST['ctf'][$this->Name()]['start'] + min($this->pageSize, $this->TotalCount() - $_REQUEST['ctf'][$this->Name()]['start']);
     875         } else {
     876             return min($this->pageSize, $this->TotalCount());
     877         }
     878     }
     879
     880     function TotalCount() {
     881         if($this->totalCount) {
     882             return $this->totalCount;
     883         }
     884         if($this->customSourceItems) {
     885             return $this->customSourceItems->Count();
     886         }
     887
     888         $this->totalCount = $this->getQuery()->unlimitedRowCount();
     889         return $this->totalCount;
     890     }
     891
     892
     893
     894     /**
     895      * #################################
     896      *           Search
     897      * #################################
     898      *
     899      * @todo Not fully implemented at the moment
     900      */
     901
     902      /**
     903       * Compile all request-parameters for search and pagination
     904       * (except the actual list-positions) as a query-string.
     905       *
     906       * @return String URL-parameters
     907       */
     908     function filterString() {
     909
     910     }
     911
     912
     913
     914     /**
     915      * #################################
     916      *           CSV Export
     917      * #################################
     918      */
     919      function setFieldListCsv($fields) {
     920         $this->fieldListCsv = $fields;
     921      }
     922
     923     /**
     924      * Set the CSV separator character.  Defaults to ,
     925      */
     926     function setCsvSeparator($csvSeparator) {
     927         $this->csvSeparator = $csvSeparator;
     928     }
     929
     930     /**
     931      * Get the CSV separator character.  Defaults to ,
     932      */
     933     function getCsvSeparator() {
     934         return $this->csvSeparator;
     935     }
     936
     937     /**
     938      * Remove the header row from the CSV export
     939      */
     940     function removeCsvHeader() {
     941         $this->csvHasHeader = false;
     942     }
     943
     944     /**
     945      * Exports a given set of comma-separated IDs (from a previous search-query, stored in a HiddenField).
     946      * Uses {$csv_columns} if present, and falls back to {$result_columns}.
     947      * We move the most filedata generation code to the function {@link generateExportFileData()} so that a child class
     948      * could reuse the filedata generation code while overwrite export function.
     949      *
     950      * @todo Make relation-syntax available (at the moment you'll have to use custom sql)
     951      */
     952     function export() {
     953         $now = Date("d-m-Y-H-i");
     954         $fileName = "export-$now.csv";
     955
     956         if($fileData = $this->generateExportFileData($numColumns, $numRows)){
     957             return SS_HTTPRequest::send_file($fileData, $fileName);
     958         }else{
     959             user_error("No records found", E_USER_ERROR);
     960         }
     961     }
     962
     963     function generateExportFileData(&$numColumns, &$numRows) {
     964         $separator = $this->csvSeparator;
     965         $csvColumns = ($this->fieldListCsv) ? $this->fieldListCsv : $this->fieldList;
     966         $fileData = "\xEF\xBB\xBF"; // Make UTF-8 + BOM
     967         $columnData = array();
     968         $fieldItems = new DataObjectSet();
     969
     970         if($this->csvHasHeader) {
     971             $fileData .= "\"" . implode("\"{$separator}\"", array_values($csvColumns)) . "\"";
     972             $fileData .= "\n";
     973         }
     974
     975         if(isset($this->customSourceItems)) {
     976             $items = $this->customSourceItems;
     977         } else {
     978             $dataQuery = $this->getCsvQuery();
     979             $items = $dataQuery->execute();
     980         }
     981
     982         // temporary override to adjust TableListField_Item behaviour
     983         $this->setFieldFormatting(array());
     984         $this->fieldList = $csvColumns;
     985
     986         if($items) {
     987             foreach($items as $item) {
     988                 if(is_array($item)) {
     989                     $className = isset($item['RecordClassName']) ? $item['RecordClassName'] : $item['ClassName'];
     990                     $item = new $className($item);
     991                 }
     992                 $fieldItem = new $this->itemClass($item, $this);
     993
     994                 $fields = $fieldItem->Fields(false);
     995                 $columnData = array();
     996                 if($fields) foreach($fields as $field) {
     997                     $value = $field->Value;
     998
     999                     // TODO This should be replaced with casting
    1000                     if(array_key_exists($field->Name, $this->csvFieldFormatting)) {
    1001                         $format = str_replace('$value', "__VAL__", $this->csvFieldFormatting[$field->Name]);
    1002                         $format = preg_replace('/\$([A-Za-z0-9-_]+)/','$item->$1', $format);
    1003                         $format = str_replace('__VAL__', '$value', $format);
    1004                         eval('$value = "' . $format . '";');
    1005                     }
    1006
    1007                     $value = str_replace(array("\r", "\n"), "\n", $value);
    1008                     $value = str_replace("\n", "\r\n", $value);
    1009                     $tmpColumnData = '"' . str_replace('"', '""', $value) . '"';
    1010                     $columnData[] = $tmpColumnData;
    1011                 }
    1012                 $fileData .= implode($separator, $columnData);
    1013                 $fileData .= "\r\n";
    1014
    1015                 $item->destroy();
    1016                 unset($item);
    1017                 unset($fieldItem);
    1018             }
    1019
    1020             $numColumns = count($columnData);
    1021             $numRows = $fieldItems->count();
    1022             return $fileData;
    1023         } else {
    1024             return null;
    1025         }
    1026     }
    1027
    1028     /**
    1029      * We need to instanciate this button manually as a normal button has no means of adding inline onclick-behaviour.
    1030      */
    1031     function ExportLink() {
    1032         $exportLink = Controller::join_links($this->Link(), 'export');
    1033
    1034         if($this->extraLinkParams) $exportLink .= "?" . http_build_query($this->extraLinkParams);
    1035         return $exportLink;
    1036     }
    1037
    1038     function printall() {
    1039         Requirements::clear();
    1040         Requirements::css(CMS_DIR . '/css/typography.css');
    1041         Requirements::css(CMS_DIR . '/css/cms_right.css');
    1042         Requirements::css(SAPPHIRE_DIR . '/css/TableListField_print.css');
    1043
    1044         $this->cachedSourceItems = null;
    1045         $oldShowPagination = $this->showPagination;
    1046         $this->showPagination = false;
    1047
    1048         increase_time_limit_to();
    1049         $this->Print = true;
    1050
    1051         $result = $this->renderWith(array($this->template . '_printable', 'TableListField_printable'));
    1052
    1053         $this->showPagination = $oldShowPagination;
    1054
    1055         return $result;
    1056     }
    1057
    1058     function PrintLink() {
    1059         $link = Controller::join_links($this->Link(), 'printall');
    1060         if(isset($_REQUEST['ctf'][$this->Name()]['sort'])) {
    1061             $link = HTTP::setGetVar("ctf[{$this->Name()}][sort]",Convert::raw2xml($_REQUEST['ctf'][$this->Name()]['sort']), $link);
    1062         }
    1063         return $link;
    1064     }
    1065
    1066     /**
    1067      * #################################
    1068      *           Utilty
    1069      * #################################
    1070      */
    1071     function Utility() {
    1072         $links = new DataObjectSet();
    1073         if($this->can('export')) {
    1074             $links->push(new ArrayData(array(
    1075                 'Title' => _t('TableListField.CSVEXPORT', 'Export to CSV'),
    1076                 'Link' => $this->ExportLink()
    1077             )));
    1078         }
    1079         if($this->can('print')) {
    1080             $links->push(new ArrayData(array(
    1081                 'Title' => _t('TableListField.PRINT', 'Print'),
    1082                 'Link' => $this->PrintLink()
    1083             )));
    1084         }
    1085         return $links;
    1086
    1087     }
    1088
    1089     /**
    1090      * Returns the content of the TableListField as a piece of FormResponse javascript
    1091      * @deprecated Please use the standard URL through Link() which gives you the FieldHolder as an HTML fragment.
    1092      */
    1093     function ajax_refresh() {
    1094         // compute sourceItems here instead of Items() to ensure that
    1095         // pagination and filters are respected on template accessors
    1096         //$this->sourceItems();
    1097
    1098         $response = $this->renderWith($this->template);
    1099         FormResponse::update_dom_id($this->id(), $response, 1);
    1100         FormResponse::set_non_ajax_content($response);
    1101         return FormResponse::respond();
    1102     }
    1103
    1104     function setFieldCasting($casting) {
    1105         $this->fieldCasting = $casting;
    1106     }
    1107
    1108     function setFieldFormatting($formatting) {
    1109         $this->fieldFormatting = $formatting;
    1110     }
    1111
    1112     function setCSVFieldFormatting($formatting) {
    1113         $this->csvFieldFormatting = $formatting;
    1114     }
    1115
    1116     /**
    1117      * Edit the field list
    1118      */
    1119     function setFieldList($fieldList) {
    1120         $this->fieldList = $fieldList;
    1121     }
    1122
    1123     /**
    1124      * @return String
    1125      */
    1126     function Name() {
    1127         return $this->name;
    1128     }
    1129
    1130     function Title() {
    1131       // adding translating functionality
    1132       // this is a bit complicated, because this parameter is passed to this class
    1133       // and should come here translated already
    1134       // adding this to TODO probably add a method to the classes
    1135       // to return they're translated string
    1136       // added by ruibarreiros @ 27/11/2007
    1137         return $this->sourceClass ? singleton($this->sourceClass)->i18n_plural_name() : $this->Name();
    1138     }
    1139
    1140     function NameSingular() {
    1141       // same as Title()
    1142       // added by ruibarreiros @ 27/11/2007
    1143       return $this->sourceClass ? singleton($this->sourceClass)->i18n_singular_name() : $this->Name();
    1144     }
    1145
    1146     function NamePlural() {
    1147       // same as Title()
    1148       // added by ruibarreiros @ 27/11/2007
    1149         return $this->sourceClass ? singleton($this->sourceClass)->i18n_plural_name() : $this->Name();
    1150     }
    1151
    1152     function setTemplate($template) {
    1153         $this->template = $template;
    1154     }
    1155
    1156     function CurrentLink() {
    1157         $link = $this->Link();
    1158
    1159         if(isset($_REQUEST['ctf'][$this->Name()]['start']) && is_numeric($_REQUEST['ctf'][$this->Name()]['start'])) {
    1160             $start = ($_REQUEST['ctf'][$this->Name()]['start'] < 0)  ? 0 : $_REQUEST['ctf'][$this->Name()]['start'];
    1161             $link = Controller::join_links($link, "?ctf[{$this->Name()}][start]={$start}");
    1162         }
    1163
    1164         if($this->extraLinkParams) $link .= "&" . http_build_query($this->extraLinkParams);
    1165
    1166         return $link;
    1167     }
    1168
    1169     function BaseLink() {
    1170         user_error("TableListField::BaseLink() deprecated, use Link() instead", E_USER_NOTICE);
    1171         return $this->Link();
    1172     }
    1173
    1174     /**
    1175      * @return Int
    1176      */
    1177     function sourceID() {
    1178         $idField = $this->form->dataFieldByName('ID');
    1179         if(!isset($idField)) {
    1180             user_error("TableListField needs a formfield named 'ID' to be present", E_USER_ERROR);
    1181         }
    1182         return $idField->Value();
    1183     }
    1184
    1185     /**
    1186      * Helper method to determine permissions for a scaffolded
    1187      * TableListField (or subclasses) - currently used in {@link ModelAdmin} and {@link DataObject->scaffoldFormFields()}.
    1188      * Returns true for each permission that doesn't have an explicit getter.
    1189      *
    1190      * @todo Temporary method, implement directly in FormField subclasses with object-level permissions.
    1191      *
    1192      * @param string $class
    1193      * @param numeric $id
    1194      * @return array
    1195      */
    1196     public static function permissions_for_object($class, $id = null) {
    1197         $permissions = array();
    1198         $obj = ($id) ? DataObject::get_by_id($class, $id) : singleton($class);
    1199
    1200         if(!$obj->hasMethod('canView') || $obj->canView()) $permissions[] = 'show';
    1201         if(!$obj->hasMethod('canEdit') || $obj->canEdit()) $permissions[] = 'edit';
    1202         if(!$obj->hasMethod('canDelete') || $obj->canDelete()) $permissions[] = 'delete';
    1203         if(!$obj->hasMethod('canCreate') || $obj->canCreate()) $permissions[] = 'add';
    1204
    1205         return $permissions;
    1206     }
    1207
    1208     /**
    1209      * @param $value
    1210      *
    1211      */
    1212     function getCastedValue($value, $castingDefinition) {
    1213         if(is_array($castingDefinition)) {
    1214             $castingParams = $castingDefinition;
    1215             array_shift($castingParams);
    1216             $castingDefinition = array_shift($castingDefinition);
    1217         } else {
    1218             $castingParams = array();
    1219         }
    1220         if(strpos($castingDefinition,'->') === false) {
    1221             $castingFieldType = $castingDefinition;
    1222             $castingField = DBField::create($castingFieldType, $value);
    1223             $value = call_user_func_array(array($castingField,'XML'),$castingParams);
    1224         } else {
    1225             $fieldTypeParts = explode('->', $castingDefinition);
    1226             $castingFieldType = $fieldTypeParts[0];
    1227             $castingMethod = $fieldTypeParts[1];
    1228             $castingField = DBField::create($castingFieldType, $value);
    1229             $value = call_user_func_array(array($castingField,$castingMethod),$castingParams);
    1230         }
    1231
    1232         return $value;
    1233     }
    1234
    1235     /**
    1236      * #########################
    1237      *       Highlighting
    1238      * #########################
    1239      */
    1240     function setHighlightConditions($conditions) {
    1241         $this->highlightConditions = $conditions;
    1242     }
    1243 }
    1244
    1245 /**
    1246  * A single record in a TableListField.
    1247  * @package forms
    1248  * @subpackage fields-relational
    1249  * @see TableListField
    1250  */
    1251 class TableListField_Item extends ViewableData {
    1252
    1253     /**
    1254      * @var DataObject The underlying data record,
    1255      * usually an element of {@link TableListField->sourceItems()}.
    1256      */
    1257     protected $item;
    1258
    1259     /**
    1260      * @var TableListField
    1261      */
    1262     protected $parent;
    1263
    1264     function __construct($item, $parent) {
    1265         $this->failover = $this->item = $item;
    1266         $this->parent = $parent;
    1267         parent::__construct();
    1268     }
    1269
    1270     function ID() {
    1271         return $this->item->ID;
    1272     }
    1273
    1274     function Parent() {
    1275         return $this->parent;
    1276     }
    1277
    1278     function Fields($xmlSafe = true) {
    1279         $list = $this->parent->FieldList();
    1280         foreach($list as $fieldName => $fieldTitle) {
    1281             $value = "";
    1282
    1283             // This supports simple FieldName syntax
    1284             if(strpos($fieldName,'.') === false) {
    1285                 $value = ($this->item->XML_val($fieldName) && $xmlSafe) ? $this->item->XML_val($fieldName) : $this->item->RAW_val($fieldName);
    1286             // This support the syntax fieldName = Relation.RelatedField
    1287             } else {
    1288                 $fieldNameParts = explode('.', $fieldName)  ;
    1289                 $tmpItem = $this->item;
    1290                 for($j=0;$j<sizeof($fieldNameParts);$j++) {
    1291                     $relationMethod = $fieldNameParts[$j];
    1292                     $idField = $relationMethod . 'ID';
    1293                     if($j == sizeof($fieldNameParts)-1) {
    1294                         if($tmpItem) $value = $tmpItem->$relationMethod;
    1295                     } else {
    1296                         if($tmpItem) $tmpItem = $tmpItem->$relationMethod();
    1297                     }
    1298                 }
    1299             }
    1300
    1301             // casting
    1302             if(array_key_exists($fieldName, $this->parent->fieldCasting)) {
    1303                 $value = $this->parent->getCastedValue($value, $this->parent->fieldCasting[$fieldName]);
    1304             } elseif(is_object($value) && method_exists($value, 'Nice')) {
    1305                 $value = $value->Nice();
    1306             }
    1307
    1308             // formatting
    1309             $item = $this->item;
    1310             if(array_key_exists($fieldName, $this->parent->fieldFormatting)) {
    1311                 $format = str_replace('$value', "__VAL__", $this->parent->fieldFormatting[$fieldName]);
    1312                 $format = preg_replace('/\$([A-Za-z0-9-_]+)/','$item->$1', $format);
    1313                 $format = str_replace('__VAL__', '$value', $format);
    1314                 eval('$value = "' . $format . '";');
    1315             }
    1316
    1317             //escape
    1318             if($escape = $this->parent->fieldEscape){
    1319                 foreach($escape as $search => $replace){
    1320                     $value = str_replace($search, $replace, $value);
    1321                 }
    1322             }
    1323
    1324             $fields[] = new ArrayData(array(
    1325                 "Name" => $fieldName,
    1326                 "Title" => $fieldTitle,
    1327                 "Value" => $value,
    1328                 "CsvSeparator" => $this->parent->getCsvSeparator(),
    1329             ));
    1330         }
    1331         return new DataObjectSet($fields);
    1332     }
    1333
    1334     function Markable() {
    1335         return $this->parent->Markable;
    1336     }
    1337
    1338     /**
    1339      * Checks global permissions for field in  {@link TableListField->Can()}.
    1340      * If they are allowed, it checks for object permissions by assuming
    1341      * a method with "can" + $mode parameter naming, e.g. canDelete().
    1342      *
    1343      * @param string $mode See {@link TableListField::$permissions} array.
    1344      * @return boolean
    1345      */
    1346     function Can($mode) {
    1347         $canMethod = "can" . ucfirst($mode);
    1348         if(!$this->parent->Can($mode)) {
    1349             // check global settings for the field instance
    1350             return false;
    1351         } elseif($this->item->hasMethod($canMethod)) {
    1352             // if global allows, check object specific permissions (e.g. canDelete())
    1353             return $this->item->$canMethod();
    1354         } else {
    1355             // otherwise global allowed this action, so return TRUE
    1356             return true;
    1357         }
    1358     }
    1359
    1360     function Link($action = null) {
    1361         if($this->parent->getForm()) {
    1362             $parentUrlParts = parse_url($this->parent->Link());
    1363             $queryPart = (isset($parentUrlParts['query'])) ? '?' . $parentUrlParts['query'] : null;
    1364             return Controller::join_links($parentUrlParts['path'], 'item', $this->item->ID, $action, $queryPart);
    1365         } else {
    1366             // allow for instanciation of this FormField outside of a controller/form
    1367             // context (e.g. for unit tests)
    1368             return false;
    1369         }
    1370     }
    1371
    1372     /**
    1373      * Returns all row-based actions not disallowed through permissions.
    1374      * See TableListField->Action for a similiar dummy-function to work
    1375      * around template-inheritance issues.
    1376      *
    1377      * @return DataObjectSet
    1378      */
    1379     function Actions() {
    1380         $allowedActions = new DataObjectSet();
    1381         foreach($this->parent->actions as $actionName => $actionSettings) {
    1382             if($this->parent->Can($actionName)) {
    1383                 $allowedActions->push(new ArrayData(array(
    1384                     'Name' => $actionName,
    1385                     'Link' => $this->{ucfirst($actionName).'Link'}(),
    1386                     'Icon' => $actionSettings['icon'],
    1387                     'IconDisabled' => $actionSettings['icon_disabled'],
    1388                     'Label' => $actionSettings['label'],
    1389                     'Class' => $actionSettings['class'],
    1390                     'Default' => ($actionName == $this->parent->defaultAction),
    1391                     'IsAllowed' => $this->Can($actionName),
    1392                 )));
    1393             }
    1394         }
    1395
    1396         return $allowedActions;
    1397     }
    1398
    1399     function BaseLink() {
    1400         user_error("TableListField_Item::BaseLink() deprecated, use Link() instead", E_USER_NOTICE);
    1401         return $this->Link();
    1402     }
    1403
    1404     function DeleteLink() {
    1405         return Controller::join_links($this->Link(), "delete");
    1406     }
    1407
    1408     function MarkingCheckbox() {
    1409         $name = $this->parent->Name() . '[]';
    1410
    1411         if($this->parent->isReadonly())
    1412             return "<input class=\"checkbox\" type=\"checkbox\" name=\"$name\" value=\"{$this->item->ID}\" disabled=\"disabled\" />";
    1413         else
    1414             return "<input class=\"checkbox\" type=\"checkbox\" name=\"$name\" value=\"{$this->item->ID}\" />";
    1415     }
    1416
    1417     function HighlightClasses() {
    1418         $classes = array();
    1419         foreach($this->parent->highlightConditions as $condition) {
    1420             $rule = str_replace("\$","\$this->item->", $condition['rule']);
    1421             $ruleApplies = null;
    1422             eval('$ruleApplies = ('.$rule.');');
    1423             if($ruleApplies) {
    1424                 if(isset($condition['exclusive']) && $condition['exclusive']) {
    1425                     return $condition['class'];
    1426                 } else {
    1427                     $classes[] = $condition['class'];
    1428                 }
    1429             }
    1430         }
    1431
    1432         return (count($classes) > 0) ? " " . implode(" ", $classes) : false;
    1433     }
    1434
    1435     /**
    1436      * Legacy: Please use permissions instead
    1437      */
    1438     function isReadonly() {
    1439         return $this->parent->Can('delete');
    1440     }
    1441 }
    1442
    1443 /**
    1444  * @package forms
    1445  * @subpackage fields-relational
    1446  */
    1447 class TableListField_ItemRequest extends RequestHandler {
    1448     protected $ctf;
    1449     protected $itemID;
    1450     protected $methodName;
    1451
    1452     static $url_handlers = array(
    1453         '$Action!' => '$Action',
    1454         '' => 'index',
    1455     );
    1456
    1457     function Link() {
    1458         return Controller::join_links($this->ctf->Link(), 'item/' . $this->itemID);
    1459     }
    1460
    1461     function __construct($ctf, $itemID) {
    1462         $this->ctf = $ctf;
    1463         $this->itemID = $itemID;
    1464
    1465         parent::__construct();
    1466     }
    1467
    1468     function delete() {
    1469         if($this->ctf->Can('delete') !== true) {
    1470             return false;
    1471         }
    1472
    1473         $this->dataObj()->delete();
    1474     }
    1475
    1476     ///////////////////////////////////////////////////////////////////////////////////////////////////
    1477
    1478     /**
    1479      * Return the data object being manipulated
    1480      */
    1481     function dataObj() {
    1482         // used to discover fields if requested and for population of field
    1483         if(is_numeric($this->itemID)) {
    1484             // we have to use the basedataclass, otherwise we might exclude other subclasses
    1485             return DataObject::get_by_id(ClassInfo::baseDataClass(Object::getCustomClass($this->ctf->sourceClass())), $this->itemID);
    1486         }
    1487
    1488     }
    1489
    1490     /**
    1491      * Returns the db-fieldname of the currently used has_one-relationship.
    1492      */
    1493     function getParentIdName( $parentClass, $childClass ) {
    1494         return $this->getParentIdNameRelation( $childClass, $parentClass, 'has_one' );
    1495     }
    1496
    1497     /**
    1498      * Manually overwrites the parent-ID relations.
    1499      * @see setParentClass()
    1500      *
    1501      * @param String $str Example: FamilyID (when one Individual has_one Family)
    1502      */
    1503     function setParentIdName($str) {
    1504         $this->parentIdName = $str;
    1505     }
    1506
    1507     /**
    1508      * Returns the db-fieldname of the currently used relationship.
    1509      */
    1510     function getParentIdNameRelation($parentClass, $childClass, $relation) {
    1511         if($this->parentIdName) return $this->parentIdName;
    1512
    1513         $relations = singleton($parentClass)->$relation();
    1514         $classes = ClassInfo::ancestry($childClass);
    1515         foreach($relations as $k => $v) {
    1516             if(array_key_exists($v, $classes)) return $k . 'ID';
    1517         }
    1518         return false;
    1519     }
    1520
    1521     /**
    1522      * @return TableListField
    1523      */
    1524     function getParentController() {
    1525         return $this->ctf;
    1526     }
    1527 }
    1528 ?>
    1529

[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the Silverstripe Forums to ask development related questions. -