Heading Text (eg. heading1) * @param $sourceFilter string The filter field you wish to limit the objects by (eg. parentID) * @param $sourceSort string * @param $sourceJoin string */ class TableListField extends FormField { /** * @var $cachedSourceItems DataObjectSet Prevent {@sourceItems()} from being called multiple times. */ protected $cachedSourceItems; protected $sourceClass; protected $sourceFilter = array(); protected $sourceSort = ""; protected $sourceJoin = array(); protected $fieldList; /** * @var $fieldListCsv array */ protected $fieldListCsv; /** * @var $clickAction */ protected $clickAction; /** * @var bool */ public $IsReadOnly; /** * Called method (needs to be retained for AddMode()) */ protected $methodName; /** * @var $summaryFieldList array Shows a row which summarizes the contents of a column by a predefined * Javascript-function */ protected $summaryFieldList; /** * @var $summaryTitle string The title which will be shown in the first column of the summary-row. * Accordingly, the first column can't be used for summarizing. */ protected $summaryTitle; /** * @var $template string Template-Overrides */ protected $template = "TableListField"; /** * @var bool Do we use checkboxes to mark records, or delete them one by one? */ public $Markable; /** * @var $readOnly boolean Deprecated, please use $permssions instead */ protected $readOnly; /** * @var $permissions array Influence output without having to subclass the template. */ protected $permissions = array( //"print", //"export", "delete" ); /** * @var $customQuery Specify custom query, e.g. for complicated having/groupby-constructs. * Caution: TableListField automatically selects the ID from the {@sourceClass}, because it relies * on this information e.g. in saving a TableField. Please use a custom select if you want to filter * for other IDs in joined tables: $query->select[] = "MyJoinedTable.ID AS MyJoinedTableID" */ protected $customQuery; /** * @var $customCsvQuery Query for CSV-export (might need different fields or further filtering) */ protected $customCsvQuery; /** * @var $customSourceItems DataObjectSet Use the manual setting of a result-set only as a last-resort * for sets which can't be resolved in a single query. * * TODO: Add pagination support for customSourceItems. */ protected $customSourceItems; /** * Character to seperate exported columns in the CSV file */ protected $csvSeparator = ";"; /* * Boolean deciding whether to include a header row in the CSV file */ protected $csvHasHeader = true; /** * @var int Shows total count regardless or pagination */ protected $totalCount; /** * @var boolean Trigger pagination */ protected $showPagination = false; /** * @var int Number of items to show on a single page (needed for pagination) */ protected $pageSize = 10; /** * @var array Definitions for highlighting table-rows with a specific class. You can use all column-names * in the result of a query. Use in combination with {@setCustomQuery} to select custom properties and joined objects. * * Example: * array( * array( * "rule" => '$Flag == "red"', * "class" => "red" * ), * array( * "rule" => '$Flag == "orange"', * "class" => "orange" * ) * ) */ public $highlightConditions = array(); /** * @var array Specify castings with fieldname as the key, and the desired casting as value. * Example: array("MyCustomDate"=>"Date","MyShortText"=>"Text->FirstSentence") */ public $fieldCasting = array(); /** * @var array Specify custom formatting for fields, e.g. to render a link instead of pure text. * Caution: Make sure to escape special php-characters like in a normal php-statement. * Example: "myFieldName" => '$ID' */ public $fieldFormatting = array(); /** * @var string */ public $exportButtonLabel = 'Export as CSV'; /** * @var string $groupByField Used to group by a specific column in the DataObject * and create partial summaries. */ public $groupByField = null; function __construct($name, $sourceClass, $fieldList, $sourceFilter = null, $sourceSort = null, $sourceJoin = null) { $this->fieldList = $fieldList; $this->sourceClass = $sourceClass; $this->sourceFilter = $sourceFilter; $this->sourceSort = $sourceSort; $this->sourceJoin = $sourceJoin; $this->readOnly = false; parent::__construct($name); Requirements::javascript('jsparty/prototype.js'); Requirements::javascript('jsparty/behaviour.js'); Requirements::javascript('jsparty/prototype_improvements.js'); Requirements::javascript('jsparty/scriptaculous/effects.js'); Requirements::javascript('sapphire/javascript/TableListField.js'); Requirements::css('sapphire/css/TableListField.css'); } function FieldHolder() { if($this->clickAction) { $id = $this->id(); Requirements::customScript(<<clickAction return false; } } }); JS );} return $this->renderWith($this->template); } function Headings() { foreach($this->fieldList as $fieldName => $fieldTitle) { $isSorted = (isset($_REQUEST['ctf'][$this->Name()]['sort']) && $fieldName == $_REQUEST['ctf'][$this->Name()]['sort']); // we can't allow sorting with partial summaries (groupByField) $isSortable = ($this->form && $this->isFieldSortable($fieldName) && !$this->groupByField); // sorting links (only if we have a form to refresh with) if($this->form) { $sortLink = $this->BaseLink(); $sortLink = HTTP::setGetVar("ctf[{$this->Name()}][sort]", $fieldName, $sortLink); if(isset($_REQUEST['ctf'][$this->Name()]['dir'])) { $XML_sort = (isset($_REQUEST['ctf'][$this->Name()]['dir'])) ? Convert::raw2xml($_REQUEST['ctf'][$this->Name()]['dir']) : null; $sortLink = HTTP::setGetVar("ctf[{$this->Name()}][dir]", $XML_sort, $sortLink); } } else { $sortLink = '#'; } $headings[] = new ArrayData(array( "Name" => $fieldName, "Title" => $fieldTitle, "IsSortable" => $isSortable, "SortLink" => $sortLink, "SortBy" => $isSorted, "SortDirection" => (isset($_REQUEST['ctf'][$this->Name()]['dir'])) ? $_REQUEST['ctf'][$this->Name()]['dir'] : null )); } return new DataObjectSet($headings); } /** * Determines if a field is "sortable". * If the field is generated by a custom getter, we can't sort on it * without generating all objects first (which would be a huge performance impact). * * @param string $fieldName * @return bool */ function isFieldSortable($fieldName) { if($this->customSourceItems) { return false; } if($this->__cachedSQL) { $query = $this->__cachedQuery; } else { $query = $this->__cachedQuery = $this->getQuery(); } $sql = $query->sql(); $SQL_fieldName = Convert::raw2sql($fieldName); return (in_array($SQL_fieldName,$query->select) || stripos($sql,"AS {$SQL_fieldName}")); } /** * Provide a custom query to compute sourceItems. This is the preferred way to using * {@setSourceItems}, because we can still paginate. * Caution: Other parameters such as {@sourceFilter} will be ignored. * Please use this only as a fallback for really complex queries (e.g. involving HAVING and GROUPBY). * * @param $query Query */ function setCustomQuery($query) { $this->customQuery = $query; } function setCustomCsvQuery($query) { $this->customCsvQuery = $query; } function setCustomSourceItems($items) { $this->customSourceItems = $items; } function sourceItems() { $SQL_limit = ($this->pageSize) ? "{$this->pageSize}" : "0"; if(is_numeric($_REQUEST['ctf'][$this->Name()]['start'])) { $SQL_start = (isset($_REQUEST['ctf'][$this->Name()]['start'])) ? intval($_REQUEST['ctf'][$this->Name()]['start']) : "0"; } if(isset($this->customSourceItems)) { if($this->customSourceItems) { $items = $this->customSourceItems->getRange($SQL_start, $SQL_limit); } else { $items = false; } } elseif(isset($this->cachedSourceItems)) { $items = $this->cachedSourceItems; } else { // get query $dataQuery = $this->getQuery(); // we don't limit when doing certain actions if(!in_array($_REQUEST['methodName'],array('printall','export'))) { $dataQuery->limit = $SQL_limit; if(isset($SQL_start)) { $dataQuery->limit .= " OFFSET {$SQL_start}"; } } // get data $records = $dataQuery->execute(); $sourceClass = $this->sourceClass; $dataobject = new $sourceClass(); $items = $dataobject->buildDataObjectSet($records, 'DataObjectSet'); $this->cachedSourceItems = $items; } return $items; } function Items() { $fieldItems = new DataObjectSet(); if($items = $this->sourceItems()) foreach($items as $item) { if($item) $fieldItems->push(new TableListField_Item($item, $this)); } return $fieldItems; } /** * Generates the query for sourceitems (without pagination/limit-clause) * * @return string */ function getQuery() { if($this->customQuery) { $query = $this->customQuery; $baseClass = ClassInfo::baseDataClass($this->sourceClass); $query->select[] = "{$baseClass}.ID AS ID"; $query->select[] = "{$baseClass}.ClassName AS ClassName"; $query->select[] = "{$baseClass}.ClassName AS RecordClassName"; } else { $query = singleton($this->sourceClass)->extendedSQL($this->sourceFilter, $this->sourceSort, null, $this->sourceJoin); // Add more selected fields if they are from joined table. $SNG = singleton($this->sourceClass); foreach($this->FieldList() as $k=>$title){ if(!$SNG->hasField($k) && !$SNG->hasMethod('get' . $k) && !strpos($k, ".")) $query->select[] = $k; } } if(isset($_REQUEST['ctf'][$this->Name()]['sort'])) { $SQL_sort = Convert::raw2sql($_REQUEST['ctf'][$this->Name()]['sort']); $sql = $query->sql(); // see {isFieldSortable} if(in_array($SQL_sort,$query->select) || stripos($sql,"AS {$SQL_sort}")) { $query->orderby = $SQL_sort; } } return clone $query; } function getCsvQuery() { $baseClass = ClassInfo::baseDataClass($this->sourceClass); if($this->customCsvQuery) { $query = $this->customCsvQuery; $query->select[] = "{$baseClass}.ID AS ID"; $query->select[] = "{$baseClass}.ClassName AS ClassName"; $query->select[] = "{$baseClass}.ClassName AS RecordClassName"; } else if($this->customQuery) { $query = $this->customQuery; $query->select[] = "{$baseClass}.ID AS ID"; $query->select[] = "{$baseClass}.ClassName AS ClassName"; $query->select[] = "{$baseClass}.ClassName AS RecordClassName"; } else { $query = singleton($this->sourceClass)->extendedSQL($this->sourceFilter, $this->sourceSort, null, $this->sourceJoin); // Add more selected fields if they are from joined table. foreach($this->FieldList() as $k=>$title){ if(!singleton($this->sourceClass)->hasField($k)) $query->select[] = $k; } } return clone $query; } function FieldList() { return $this->fieldList; } /** * Configure this table to load content into a subform via ajax */ function setClick_AjaxLoad($urlBase, $formID) { $this->clickAction = "this.ajaxRequest('" . addslashes($urlBase) . "', '" . addslashes($formID) . "')"; } /** * Configure this table to open a popup window */ function setClick_PopupLoad($urlBase) { $this->clickAction = "var w = window.open(baseHref() + '$urlBase' + this.id.replace('record-',''), 'popup'); w.focus();"; } function performReadonlyTransformation() { $this->setShowPagination(false); $this->setPermissions(array()); $this->IsReadOnly = true; return $this; } /** * ################################# * CRUD * ################################# */ /** * @return String */ function delete() { if($this->Can('delete') !== true) { return false; } $this->methodName = "delete"; $childId = Convert::raw2sql($_REQUEST['ctf']['childID']); if (is_numeric($childId)) { $childObject = DataObject::get_by_id($this->sourceClass, $childId); if($childObject) $childObject->delete(); } // TODO return status in JSON etc. //return $this->renderWith($this->template); } /** * ################################# * Summary-Row * ################################# */ /** * Can utilize some built-in summary-functions, with optional casting. * Currently supported: * - sum * - avg * * @param $summaryTitle string * @param $summaryFields array * Simple Format: array("MyFieldName"=>"sum") * With Casting: array("MyFieldname"=>array("sum","Currency->Nice")) */ function addSummary($summaryTitle, $summaryFieldList) { $this->summaryTitle = $summaryTitle; $this->summaryFieldList = $summaryFieldList; } function removeSummary() { $this->summaryTitle = null; $this->summaryFields = null; } function HasSummary() { return (isset($this->summaryFieldList)); } function SummaryTitle() { return $this->summaryTitle; } /** * @param DataObjectSet $items Only used to pass grouped sourceItems for creating * partial summaries. */ function SummaryFields($items = null) { if(!isset($this->summaryFieldList)) { return false; } $summaryFields = array(); $fieldListWithoutFirst = $this->fieldList; if(!empty($this->summaryTitle)) { array_shift($fieldListWithoutFirst); } foreach($fieldListWithoutFirst as $fieldName => $fieldTitle) { if(in_array($fieldName, array_keys($this->summaryFieldList))) { if(is_array($this->summaryFieldList[$fieldName])) { $summaryFunction = "colFunction_{$this->summaryFieldList[$fieldName][0]}"; $casting = $this->summaryFieldList[$fieldName][1]; } else { $summaryFunction = "colFunction_{$this->summaryFieldList[$fieldName]}"; $casting = null; } // fall back to integrated sourceitems if not passed if(!$items) $items = $this->sourceItems(); $summaryValue = ($items) ? $this->$summaryFunction($items->column($fieldName)) : null; // Optional casting, Format: array('MyFieldName'=>array('sum','Currency->Nice')) if(isset($casting)) { $fieldTypeParts = explode('->', $casting); $castingFieldType = $fieldTypeParts[0]; $castingMethod = $fieldTypeParts[1]; $summaryValue = DBField::create($castingFieldType, $summaryValue)->$castingMethod(); } } else { $summaryValue = null; $function = null; } $summaryFields[] = new ArrayData(array( 'Function' => $function, 'SummaryValue' => $summaryValue, )); } return new DataObjectSet($summaryFields); } function HasGroupedItems() { return ($this->groupByField); } function GroupedItems() { if(!$this->groupByField) { return false; } $items = $this->sourceItems(); if(!$items || !$items->Count()) { return false; } $groupedItems = $items->groupBy($this->groupByField); $groupedArrItems = new DataObjectSet(); foreach($groupedItems as $key => $group) { $fieldItems = new DataObjectSet(); foreach($group as $item) { if($item) $fieldItems->push(new TableListField_Item($item, $this)); } $groupedArrItems->push(new ArrayData(array( 'Items' => $fieldItems, 'SummaryFields' => $this->SummaryFields($group) ))); } return $groupedArrItems; } function colFunction_sum($values) { return array_sum($values); } function colFunction_avg($values) { return array_sum($values)/count($values); } /** * ################################# * Permissions * ################################# */ /** * Template accessor for Permissions */ function Can($mode) { if($mode == 'add' && $this->IsReadOnly) { return false; } else if($mode == 'delete' && $this->IsReadOnly) { return false; } else if($mode == 'edit' && $this->IsReadOnly) { return false; } else { return (in_array($mode, $this->permissions)); } } function setPermissions($arr) { $this->permissions = $arr; } /** * @return array */ function getPermissions($arr) { return $this->permissions; } /** * ################################# * Pagination * ################################# */ function setShowPagination($bool) { $this->showPagination = (bool)$bool; } /** * @return boolean */ function ShowPagination() { if($this->showPagination && !empty($this->summaryFieldList)) { user_error("You can't combine pagination and summaries - please disable one of them.", E_USER_ERROR); } return $this->showPagination; } function setPageSize($pageSize) { $this->pageSize = $pageSize; } function PageSize() { return $this->pageSize; } function ListStart() { return $_REQUEST['ctf'][$this->Name()]['start']; } function FirstLink() { $start = 0; if(!isset($_REQUEST['ctf'][$this->Name()]['start']) || !is_numeric($_REQUEST['ctf'][$this->Name()]['start']) || $_REQUEST['ctf'][$this->Name()]['start'] == 0) { return null; } return $this->BaseLink() . "&ctf[{$this->Name()}][start]={$start}{$this->filterString()}"; } function PrevLink() { $currentStart = isset($_REQUEST['ctf'][$this->Name()]['start']) ? $_REQUEST['ctf'][$this->Name()]['start'] : 0; if($currentStart == 0) { return null; } $start = ($_REQUEST['ctf'][$this->Name()]['start'] - $this->pageSize < 0) ? 0 : $_REQUEST['ctf'][$this->Name()]['start'] - $this->pageSize; return $this->BaseLink() . "&ctf[{$this->Name()}][start]=$start{$this->filterString()}"; } function NextLink() { $currentStart = isset($_REQUEST['ctf'][$this->Name()]['start']) ? $_REQUEST['ctf'][$this->Name()]['start'] : 0; $start = ($currentStart + $this->pageSize < $this->TotalCount()) ? $currentStart + $this->pageSize : $this->TotalCount() % $this->pageSize > 0; if($currentStart >= $start-1) { return null; } return $this->BaseLink() . "&ctf[{$this->Name()}][start]={$start}{$this->filterString()}"; } function LastLink() { $pageSize = ($this->TotalCount() % $this->pageSize > 0) ? $this->TotalCount() % $this->pageSize : $this->pageSize; $start = $this->totalCount - $pageSize; // Check if there is only one page, or if we are on last page if($this->totalCount <= $pageSize || (isset($_REQUEST['ctf'][$this->Name()]['start']) && $_REQUEST['ctf'][$this->Name()]['start'] >= $start)) { return null; } return $this->BaseLink() . "&ctf[{$this->Name()}][start]=$start{$this->filterString()}"; } function FirstItem() { return isset($_REQUEST['ctf'][$this->Name()]['start']) ? $_REQUEST['ctf'][$this->Name()]['start'] + 1 : 1; } function LastItem() { if(isset($_REQUEST['ctf'][$this->Name()]['start'])) { return $_REQUEST['ctf'][$this->Name()]['start'] + min($this->pageSize, $this->TotalCount() - $_REQUEST['ctf'][$this->Name()]['start']); } else { return min($this->pageSize, $this->TotalCount()); } } function TotalCount() { if($this->totalCount) { return $this->totalCount; } if($this->customSourceItems) { return $this->customSourceItems->Count(); } $countQuery = $this->getQuery(); $countQuery->orderby = array(); $baseClass = ClassInfo::baseDataClass($this->sourceClass); // we can't clear the select if we're relying on its output by a HAVING clause if(count($countQuery->having) || count($countQuery->groupby)) { $records = $countQuery->execute(); // TODO figure out how to use COUNT and GROUBY together to produce a single rowcount $this->totalCount = $records->numRecords(); } else { $countQuery->select = array(); $countQuery->groupby = array(); $countQuery->select[] = "COUNT(DISTINCT {$baseClass}.ID) AS TotalCount"; $records = $countQuery->execute(); $record = $records->nextRecord(); $this->totalCount = $record['TotalCount']; } return $this->totalCount; } /** * ################################# * Search * ################################# * * TODO Not fully implemented at the moment */ /** * Compile all request-parameters for search and pagination * (except the actual list-positions) as a query-string. * * @return String URL-parameters */ function filterString() { } /** * ################################# * CSV Export * ################################# */ function setFieldListCsv($fields) { $this->fieldListCsv = $fields; } /** * Set the CSV separator character. Defaults to ; */ function setCsvSeparator($csvSeparator) { $this->csvSeparator = $csvSeparator; } /** * Remove the header row from the CSV export */ function removeCsvHeader() { $this->csvHasHeader = false; } /** * Exports a given set of comma-separated IDs (from a previous search-query, stored in a HiddenField). * Uses {$csv_columns} if present, and falls back to {$result_columns}. * * TODO Make relation-syntax available (at the moment you'll have to use custom sql) */ function export() { $now = Date("d-m-Y-H-i"); $fileName = "export-$now.csv"; $separator = $this->csvSeparator; $csvColumns = ($this->fieldListCsv) ? $this->fieldListCsv : $this->fieldList; $fileData = ""; if($this->csvHasHeader) { $fileData .= "\"" . implode("\"{$separator}\"",array_values($csvColumns)) . "\""; $fileData .= "\n"; } // get data $dataQuery = $this->getCsvQuery(); $records = $dataQuery->execute(); $sourceClass = $this->sourceClass; $dataobject = new $sourceClass(); $items = $dataobject->buildDataObjectSet($records, 'DataObjectSet'); if($items) { foreach($items as $item) { $columnData = array(); foreach($csvColumns as $columnName => $columnTitle) { $tmpColumnData = "\"" . str_replace("\"", "\"\"", $item->$columnName) . "\""; $tmpColumnData = str_replace(array("\r", "\n"), "", $tmpColumnData); $columnData[] = $tmpColumnData; } $fileData .= implode($separator, $columnData); $fileData .= "\n"; } HTTP::sendFileToBrowser($fileData, $fileName); } else { user_error("No records found", E_USER_ERROR); } } /** * We need to instanciate this button manually as a normal button has no means of adding inline onclick-behaviour. */ function ExportLink() { return Director::absoluteURL($this->FormAction()) . "&action_callfieldmethod&fieldName={$this->Name()}&methodName=export"; } function printall() { Requirements::clear(); Requirements::css('cms/css/cms_typography.css'); Requirements::css('cms/css/cms_right.css'); Requirements::css('sapphire/css/TableListField_print.css'); $vd = new ViewableData(); return $vd->customise(array( 'Content' => $this->customise(array( 'Print' => true ))->renderWith($this->template) ))->renderWith('TableListField_printable'); } function PrintLink() { $link = Director::absoluteURL($this->FormAction()) . "&action_callfieldmethod&fieldName={$this->Name()}&methodName=printall"; if(isset($_REQUEST['ctf'][$this->Name()]['sort'])) { $link = HTTP::setGetVar("ctf[{$this->Name()}][sort]",Convert::raw2xml($_REQUEST['ctf'][$this->Name()]['sort']), $link); } return $link; } /** * ################################# * Utilty * ################################# */ function Utility() { $links = new DataObjectSet(); if($this->can('export')) { $links->push(new ArrayData(array( 'Title' => 'Export to CSV', 'Link' => $this->ExportLink() ))); } if($this->can('print')) { $links->push(new ArrayData(array( 'Title' => 'Print', 'Link' => $this->PrintLink() ))); } return $links; } function ajax_refresh() { // compute sourceItems here instead of Items() to ensure that // pagination and filters are respected on template accessors //$this->sourceItems(); $response = $this->renderWith($this->template); FormResponse::update_dom_id($this->id(), $response); FormResponse::set_non_ajax_content($response); return FormResponse::respond(); } function setFieldCasting($casting) { $this->fieldCasting = $casting; } function setFieldFormatting($formatting) { $this->fieldFormatting = $formatting; } /** * @return String */ function Name() { return $this->name; } function Title() { return singleton($this->sourceClass)->singular_name(); } function NameSingular() { return singleton($this->sourceClass)->singular_name(); } function NamePlural() { return singleton($this->sourceClass)->plural_name(); } function setTemplate($template) { $this->template = $template; } function BaseLink() { return $this->FormAction() . "&action_callfieldmethod&fieldName={$this->Name()}&ctf[ID]={$this->sourceID()}&methodName=ajax_refresh"; } /** * Returns the action of the surrounding form - needed to maintain context on subsequent calls. * It is only needed to embed this field into a form if you want to use more than "display-functionality". * We try to mirror the existing GET-properties to achieve the same application-state. * * @return String */ function FormAction() { $params = $_GET; // we don't want this to be overriding our new actions unset($params['executeForm']); unset($params['fieldName']); unset($params['url']); unset($params['methodName']); unset($params['forcehtml']); // TODO Refactor unset($params['ctf']); $params['ctf'][$this->Name()]['search'] = (isset($_REQUEST['ctf'][$this->Name()]['search'])) ? $_REQUEST['ctf'][$this->Name()]['search'] : null; // unset all actions (otherwise they override action_callfieldmethod) foreach($params as $paramKey => $paramVal) { if(strpos($paramKey, 'action_') === 0) { unset($params[$paramKey]); } } try { $link = $this->form->FormAction(); $link .= (!strpos($link,'?')) ? '?' : '&'; $link .= urldecode (http_build_query($params)); } catch(Exception $e) { user_error('Please embed this field into a form if you want to use actions such as "add", "edit" or "delete"', E_USER_ERROR); } return $link; } /** * Returns a link by which we can access this form */ function FormObjectLink($formName) { return $this->form->FormAction() . ".ReferencedField.$formName&fieldName={$this->Name()}"; } /** * @return Int */ function sourceID() { $idField = $this->form->dataFieldByName('ID'); if(!isset($idField)) { user_error("TableListField needs a formfield named 'ID' to be present", E_USER_ERROR); } return $idField->Value(); } /** * Get part of class ancestry for css-class-usage. * Avoids having to subclass just to built templates with new css-classes. */ function Classes() { global $_ALL_CLASSES; $items = array(); $parents = $_ALL_CLASSES['parents'][$this->class]; foreach($parents as $parent) { if(!in_array($parent,$_ALL_CLASSES['parents']["TableListField"])) { $items[] = $parent; } } $items[] = $this->class; if($this->template != $this->class) { $items[] = $this->template; } return implode(" ", $items); } /** * ######################### * Highlighting * ######################### */ function setHighlightConditions($conditions) { $this->highlightConditions = $conditions; } } class TableListField_Item extends ViewableData { protected $item, $parent; function __construct($item, $parent) { $this->failover = $this->item = $item; $this->parent = $parent; parent::__construct(); } function ID() { return $this->item->ID; } function Parent() { return $this->parent; } function Fields() { $list = $this->parent->FieldList(); foreach($list as $fieldName => $fieldTitle) { // This supports simple FieldName syntax if(strpos($fieldName,'.') === false) { $value = ($this->item->val($fieldName)) ? $this->item->val($fieldName) : $this->item->$fieldName; // This support the syntax fieldName = Relation.RelatedField } else { $fieldNameParts = explode('.', $fieldName) ; $tmpItem = $this->item; for($j=0;$j$relationMethod; } else { $tmpItem = $tmpItem->$relationMethod(); } } } // casting if(array_key_exists($fieldName, $this->parent->fieldCasting)) { $fieldType = $this->parent->fieldCasting[$fieldName]; if(strpos($fieldType,'->') === false) { $castingFieldType = $fieldType; $castingField = new $castingFieldType($fieldName); $castingField->setValue($value); $value = $castingField->XML(); } else { $fieldTypeParts = explode('->', $fieldType); $castingFieldType = $fieldTypeParts[0]; $castingMethod = $fieldTypeParts[1]; $castingField = new $castingFieldType($fieldName); $castingField->setValue($value); $value = $castingField->$castingMethod(); } } // formatting $item = $this->item; if(array_key_exists($fieldName, $this->parent->fieldFormatting)) { $format = str_replace('$value', "__VAL__", $this->parent->fieldFormatting[$fieldName]); $format = preg_replace('/\$([A-Za-z0-9-_]+)/','$item->$1', $format); $format = str_replace('__VAL__', '$value', $format); eval('$value = "' . $format . '";'); } $fields[] = new ArrayData(array( "Name" => $fieldName, "Title" => $fieldTitle, "Value" => $value, )); } return new DataObjectSet($fields); } function Markable() { return $this->parent->Markable; } function Can($mode) { return $this->parent->Can($mode); } function BaseLink() { return $this->parent->FormAction() . "&action_callfieldmethod&fieldName={$this->parent->Name()}&tf[childID]={$this->item->ID}"; } function DeleteLink() { return $this->BaseLink() . "&methodName=delete"; } function MarkingCheckbox() { $name = $this->parent->Name() . '[]'; if($this->parent->IsReadOnly || !$this->Can('edit')) return "item->ID}\" disabled=\"disabled\" />"; else return "item->ID}\" />"; } function HighlightClasses() { $classes = array(); foreach($this->parent->highlightConditions as $condition) { $rule = str_replace("\$","\$this->item->", $condition['rule']); $ruleApplies = null; eval('$ruleApplies = ('.$rule.');'); if($ruleApplies) { if($condition['exclusive']) { return $condition['class']; } else { $classes[] = $condition['class']; } } } return (count($classes) > 0) ? " " . implode(" ", $classes) : false; } /** * Legacy: Please use permissions instead */ function IsReadOnly() { return $this->parent->Can('delete'); } } ?>