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 * @package forms * @subpackage fields-relational */ class TableListField extends FormField { /** * The {@link DataList} object defining the source data for this view/ */ protected $dataList; protected $fieldList; protected $disableSorting = false; /** * @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 $itemClass string Class name for each item/row */ public $itemClass = 'TableListField_Item'; /** * @var bool Do we use checkboxes to mark records, or delete them one by one? */ public $Markable; public $MarkableTitle = null; /** * @var array See {@link SelectOptions()} */ protected $selectOptions = array(); /** * @var $readOnly boolean Deprecated, please use $permssions instead */ protected $readOnly; /** * @var $permissions array Influence output without having to subclass the template. * See $actions for adding your custom actions/permissions. */ protected $permissions = array( //"print", //"export", "delete" ); /** * @var $actions array Action that can be performed on a single row-entry. * Has to correspond to a method in a TableListField-class (or subclass). * Actions can be disabled through $permissions. * Format (key is used for the methodname and CSS-class): * array( * 'delete' => array( * 'label' => 'Delete', * 'icon' => 'sapphire/images/delete.gif', * 'icon_disabled' => 'sapphire/images/delete_disabled.gif', * 'class' => 'deletelink', * ) * ) */ public $actions = array( 'delete' => array( 'label' => 'Delete', 'icon' => 'sapphire/images/delete.gif', 'icon_disabled' => 'sapphire/images/delete_disabled.gif', 'class' => 'deletelink' ) ); /** * @var $defaultAction String Action being executed when clicking on table-row (defaults to "show"). * Mostly needed in ComplexTableField-subclass. */ public $defaultAction = ''; /** * @var $customCsvQuery Query for CSV-export (might need different fields or further filtering) */ protected $customCsvQuery; /** * 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 array Specify custom escape for the fields. * * * array("\""=>"\"\"","\r"=>"", "\r\n"=>"", "\n"=>"") * */ public $csvFieldEscape = array( "\""=>"\"\"", "\r\n"=>"", "\r"=>"", "\n"=>"", ); /** * @var boolean Trigger pagination */ protected $showPagination = false; /** * @var string Override the {@link Link()} method * for all pagination. Useful to force rendering of the field * in a different context. */ public $paginationBaseLink = null; /** * @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(); public $csvFieldFormatting = 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; /** * @var array */ protected $extraLinkParams; protected $__cachedQuery; /** * This is a flag that enables some backward-compatibility helpers. */ private $getDataListFromForm; function __construct($name, $sourceClass = null, $fieldList = null, $sourceFilter = null, $sourceSort = null, $sourceJoin = null) { if($sourceClass) { // You can optionally pass a list if($sourceClass instanceof SS_List) { $this->dataList = $sourceClass; } else { $this->dataList = DataObject::get($sourceClass)->where($sourceFilter)->sort($sourceSort); if($sourceJoin) $this->dataList = $this->dataList->join($sourceJoin); // Grab it from the form relation, if available. $this->getDataListFromForm = true; } } $this->fieldList = ($fieldList) ? $fieldList : singleton($this->sourceClass())->summaryFields(); $this->readOnly = false; parent::__construct($name); } function index() { return $this->FieldHolder(); } static $url_handlers = array( 'item/$ID' => 'handleItem', '$Action' => '$Action', ); function sourceClass() { $list = $this->getDataList(); if(method_exists($list, 'dataClass')) return $list->dataClass(); // Failover for SS_List else return get_class($list->First()); } function handleItem($request) { return new TableListField_ItemRequest($this, $request->param('ID')); } function FieldHolder() { Requirements::javascript(SAPPHIRE_DIR . '/thirdparty/jquery/jquery.js'); Requirements::javascript(SAPPHIRE_DIR . '/thirdparty/prototype/prototype.js'); Requirements::javascript(SAPPHIRE_DIR . '/thirdparty/behaviour/behaviour.js'); Requirements::javascript(SAPPHIRE_DIR . '/javascript/prototype_improvements.js'); Requirements::add_i18n_javascript(SAPPHIRE_DIR . '/javascript/lang'); Requirements::javascript(SAPPHIRE_DIR . '/javascript/TableListField.js'); Requirements::css(SAPPHIRE_DIR . '/css/TableListField.css'); if($this->clickAction) { $id = $this->id(); Requirements::customScript(<<clickAction return false; } } }); JS );} return $this->renderWith($this->template); } function Headings() { $headings = array(); foreach($this->fieldList as $fieldName => $fieldTitle) { $isSorted = (isset($_REQUEST['ctf'][$this->getName()]['sort']) && $fieldName == $_REQUEST['ctf'][$this->getName()]['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->Link(); $sortLink = HTTP::setGetVar("ctf[{$this->getName()}][sort]", $fieldName, $sortLink,'&'); // Apply sort direction to the current sort field if(!empty($_REQUEST['ctf'][$this->getName()]['sort']) && ($_REQUEST['ctf'][$this->getName()]['sort'] == $fieldName)) { $dir = isset($_REQUEST['ctf'][$this->getName()]['dir']) ? $_REQUEST['ctf'][$this->getName()]['dir'] : null; $dir = trim(strtolower($dir)); $newDir = ($dir == 'desc') ? null : 'desc'; $sortLink = HTTP::setGetVar("ctf[{$this->getName()}][dir]", Convert::raw2xml($newDir), $sortLink,'&'); } if(isset($_REQUEST['ctf'][$this->getName()]['search']) && is_array($_REQUEST['ctf'][$this->getName()]['search'])) { foreach($_REQUEST['ctf'][$this->getName()]['search'] as $parameter => $value) { $XML_search = Convert::raw2xml($value); $sortLink = HTTP::setGetVar("ctf[{$this->getName()}][search][$parameter]", $XML_search, $sortLink,'&'); } } } else { $sortLink = '#'; } $headings[] = new ArrayData(array( "Name" => $fieldName, "Title" => ($this->sourceClass()) ? singleton($this->sourceClass())->fieldLabel($fieldTitle) : $fieldTitle, "IsSortable" => $isSortable, "SortLink" => $sortLink, "SortBy" => $isSorted, "SortDirection" => (isset($_REQUEST['ctf'][$this->getName()]['dir'])) ? $_REQUEST['ctf'][$this->getName()]['dir'] : null )); } return new ArrayList($headings); } function disableSorting($to = true) { $this->disableSorting = $to; } /** * 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->disableSorting) return false; $list = $this->getDataList(); if(method_exists($list,'canSortBy')) return $list->canSortBy($fieldName); else return false; } /** * Dummy function to get number of actions originally generated in * TableListField_Item. * * @return SS_List */ function Actions() { $allowedActions = new ArrayList(); foreach($this->actions as $actionName => $actionSettings) { if($this->Can($actionName)) { $allowedActions->push(new ViewableData()); } } return $allowedActions; } /** * Provide a custom query to compute sourceItems. This is the preferred way to using * {@setSourceItems}, because we can still paginate. * Please use this only as a fallback for really complex queries (e.g. involving HAVING and GROUPBY). * * @param $query DataList */ function setCustomQuery(DataList $dataList) { $this->dataList = $dataList; } function setCustomCsvQuery(DataList $dataList) { $this->customCsvQuery = $query; } function setCustomSourceItems(SS_List $items) { user_error('TableList::setCustomSourceItems() deprecated, just pass the items into the constructor', E_USER_WARNING); // The type-hinting above doesn't seem to work consistently if($items instanceof SS_List) { $this->dataList = $items; } else { user_error('TableList::setCustomSourceItems() should be passed a SS_List', E_USER_WARNING); } } /** * Get items, with sort & limit applied */ function sourceItems() { // get items (this may actually be a SS_List) $items = clone $this->getDataList(); // TODO: Sorting could be implemented on regular SS_Lists. if(method_exists($items,'canSortBy') && isset($_REQUEST['ctf'][$this->getName()]['sort'])) { $sort = $_REQUEST['ctf'][$this->getName()]['sort']; // TODO: sort direction if($items->canSortBy($sort)) $items = $items->sort($sort); } // Determine pagination limit, offset // To disable pagination, set $this->showPagination to false. if($this->showPagination && $this->pageSize) { $SQL_limit = (int)$this->pageSize; if(isset($_REQUEST['ctf'][$this->getName()]['start']) && is_numeric($_REQUEST['ctf'][$this->getName()]['start'])) { $SQL_start = (isset($_REQUEST['ctf'][$this->getName()]['start'])) ? intval($_REQUEST['ctf'][$this->getName()]['start']) : "0"; } else { $SQL_start = 0; } $items = $items->getRange($SQL_start, $SQL_limit); } return $items; } /** * Return a SS_List of TableListField_Item objects, suitable for display in the template. */ function Items() { $fieldItems = new ArrayList(); if($items = $this->sourceItems()) foreach($items as $item) { if($item) $fieldItems->push(new $this->itemClass($item, $this)); } return $fieldItems; } /** * Returns the DataList for this field. */ function getDataList() { // If we weren't passed in a DataList to begin with, try and get the datalist from the form if($this->form && $this->getDataListFromForm) { $this->getDataListFromForm = false; $relation = $this->name; if($record = $this->form->getRecord()) { if($record->hasMethod($relation)) $this->dataList = $record->$relation(); } } if(!$this->dataList) { user_error(get_class($this). ' is missing a DataList', E_USER_ERROR); } return $this->dataList; } function getCsvDataList() { if($this->customCsvQuery) return $this->customCsvQuery; else return $this->getDataList(); } /** * @deprecated Use getDataList() instead. */ function getQuery() { Deprecation::notice('3.0', 'Use getDataList() instead.'); $list = $this->getDataList(); if(method_exists($list,'dataQuery')) { return $this->getDataList()->dataQuery()->query(); } } /** * @deprecated Use getCsvDataList() instead. */ function getCsvQuery() { Deprecation::notice('3.0', 'Use getCsvDataList() instead.'); $list = $this->getCsvDataList(); if(method_exists($list,'dataQuery')) { return $list->dataQuery()->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(/.*-(\d*)$/,'$1'), 'popup'); w.focus();"; } function performReadonlyTransformation() { $clone = clone $this; $clone->setShowPagination(false); // Only include the show action if it was in the original CTF. $clone->setPermissions(in_array('show', $this->permissions) ? array('show') : array()); $clone->addExtraClass( 'readonly' ); $clone->setReadonly(true); return $clone; } /** * ################################# * CRUD * ################################# */ /** * @return String */ function delete($request) { // Protect against CSRF on destructive action $token = $this->getForm()->getSecurityToken(); if(!$token->checkRequest($request)) return $this->httpError('400'); if($this->Can('delete') !== true) { return false; } $this->methodName = "delete"; $childId = Convert::raw2sql($_REQUEST['ctf']['childID']); if (is_numeric($childId)) { $this->getDataList()->removeById($childId); } // 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 SS_List $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)) { $summaryValue = $this->getCastedValue($summaryValue, $casting); } } else { $summaryValue = null; $function = null; } $summaryFields[] = new ArrayData(array( 'Function' => $function, 'SummaryValue' => $summaryValue, 'Name' => DBField::create('Varchar', $fieldName), 'Title' => DBField::create('Varchar', $fieldTitle), )); } return new ArrayList($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 ArrayList(); foreach($groupedItems as $key => $group) { $fieldItems = new ArrayList(); foreach($group as $item) { if($item) $fieldItems->push(new $this->itemClass($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. * See {@link TableListField_Item->Can()} for object-specific * permissions. * * @return boolean */ 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() { 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->getName()]['start']; } /** * @param array * @deprecated Put the query string onto your form's link instead :-) */ function setExtraLinkParams($params){ Deprecation::notice('2.4', 'Put the query string onto your FormAction instead().'); $this->extraLinkParams = $params; } /** * @return array */ function getExtraLinkParams(){ return $this->extraLinkParams; } function FirstLink() { $start = 0; if(!isset($_REQUEST['ctf'][$this->getName()]['start']) || !is_numeric($_REQUEST['ctf'][$this->getName()]['start']) || $_REQUEST['ctf'][$this->getName()]['start'] == 0) { return null; } $baseLink = ($this->paginationBaseLink) ? $this->paginationBaseLink : $this->Link(); $link = Controller::join_links($baseLink, "?ctf[{$this->getName()}][start]={$start}"); if($this->extraLinkParams) $link .= "&" . http_build_query($this->extraLinkParams); // preserve sort options if(isset($_REQUEST['ctf'][$this->getName()]['sort'])) { $link .= "&ctf[{$this->getName()}][sort]=" . $_REQUEST['ctf'][$this->getName()]['sort']; // direction if(isset($_REQUEST['ctf'][$this->getName()]['dir'])) { $link .= "&ctf[{$this->getName()}][dir]=" . $_REQUEST['ctf'][$this->getName()]['dir']; } } return $link; } function PrevLink() { $currentStart = isset($_REQUEST['ctf'][$this->getName()]['start']) ? $_REQUEST['ctf'][$this->getName()]['start'] : 0; if($currentStart == 0) { return null; } $start = ($_REQUEST['ctf'][$this->getName()]['start'] - $this->pageSize < 0) ? 0 : $_REQUEST['ctf'][$this->getName()]['start'] - $this->pageSize; $baseLink = ($this->paginationBaseLink) ? $this->paginationBaseLink : $this->Link(); $link = Controller::join_links($baseLink, "?ctf[{$this->getName()}][start]={$start}"); if($this->extraLinkParams) $link .= "&" . http_build_query($this->extraLinkParams); // preserve sort options if(isset($_REQUEST['ctf'][$this->getName()]['sort'])) { $link .= "&ctf[{$this->getName()}][sort]=" . $_REQUEST['ctf'][$this->getName()]['sort']; // direction if(isset($_REQUEST['ctf'][$this->getName()]['dir'])) { $link .= "&ctf[{$this->getName()}][dir]=" . $_REQUEST['ctf'][$this->getName()]['dir']; } } return $link; } function NextLink() { $currentStart = isset($_REQUEST['ctf'][$this->getName()]['start']) ? $_REQUEST['ctf'][$this->getName()]['start'] : 0; $start = ($currentStart + $this->pageSize < $this->TotalCount()) ? $currentStart + $this->pageSize : $this->TotalCount() % $this->pageSize > 0; if($currentStart >= $start-1) { return null; } $baseLink = ($this->paginationBaseLink) ? $this->paginationBaseLink : $this->Link(); $link = Controller::join_links($baseLink, "?ctf[{$this->getName()}][start]={$start}"); if($this->extraLinkParams) $link .= "&" . http_build_query($this->extraLinkParams); // preserve sort options if(isset($_REQUEST['ctf'][$this->getName()]['sort'])) { $link .= "&ctf[{$this->getName()}][sort]=" . $_REQUEST['ctf'][$this->getName()]['sort']; // direction if(isset($_REQUEST['ctf'][$this->getName()]['dir'])) { $link .= "&ctf[{$this->getName()}][dir]=" . $_REQUEST['ctf'][$this->getName()]['dir']; } } return $link; } 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->getName()]['start']) && $_REQUEST['ctf'][$this->getName()]['start'] >= $start)) { return null; } $baseLink = ($this->paginationBaseLink) ? $this->paginationBaseLink : $this->Link(); $link = Controller::join_links($baseLink, "?ctf[{$this->getName()}][start]={$start}"); if($this->extraLinkParams) $link .= "&" . http_build_query($this->extraLinkParams); // preserve sort options if(isset($_REQUEST['ctf'][$this->getName()]['sort'])) { $link .= "&ctf[{$this->getName()}][sort]=" . $_REQUEST['ctf'][$this->getName()]['sort']; // direction if(isset($_REQUEST['ctf'][$this->getName()]['dir'])) { $link .= "&ctf[{$this->getName()}][dir]=" . $_REQUEST['ctf'][$this->getName()]['dir']; } } return $link; } function FirstItem() { if ($this->TotalCount() < 1) return 0; return isset($_REQUEST['ctf'][$this->getName()]['start']) ? $_REQUEST['ctf'][$this->getName()]['start'] + 1 : 1; } function LastItem() { if(isset($_REQUEST['ctf'][$this->getName()]['start'])) { return $_REQUEST['ctf'][$this->getName()]['start'] + min($this->pageSize, $this->TotalCount() - $_REQUEST['ctf'][$this->getName()]['start']); } else { return min($this->pageSize, $this->TotalCount()); } } /** * @ignore */ private $_cache_TotalCount; /** * Return the total number of items in the source DataList */ function TotalCount() { if($this->_cache_TotalCount === null) { $this->_cache_TotalCount = $this->getDataList()->Count(); } return $this->_cache_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; } /** * Get the CSV separator character. Defaults to , */ function getCsvSeparator() { return $this->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}. * We move the most filedata generation code to the function {@link generateExportFileData()} so that a child class * could reuse the filedata generation code while overwrite export function. * * @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"; // No pagination for export $oldShowPagination = $this->showPagination; $this->showPagination = false; $result = $this->renderWith(array($this->template . '_printable', 'TableListField_printable')); $this->showPagination = $oldShowPagination; if($fileData = $this->generateExportFileData($numColumns, $numRows)){ return SS_HTTPRequest::send_file($fileData, $fileName); }else{ user_error("No records found", E_USER_ERROR); } } function generateExportFileData(&$numColumns, &$numRows) { $separator = $this->csvSeparator; $csvColumns = ($this->fieldListCsv) ? $this->fieldListCsv : $this->fieldList; $fileData = ''; $columnData = array(); $fieldItems = new ArrayList(); if($this->csvHasHeader) { $fileData .= "\"" . implode("\"{$separator}\"", array_values($csvColumns)) . "\""; $fileData .= "\n"; } if(isset($this->customSourceItems)) { $items = $this->customSourceItems; } else { $items = $this->getCsvDataList(); } // temporary override to adjust TableListField_Item behaviour $this->setFieldFormatting(array()); $this->fieldList = $csvColumns; if($items) { foreach($items as $item) { if(is_array($item)) { $className = isset($item['RecordClassName']) ? $item['RecordClassName'] : $item['ClassName']; $item = new $className($item); } $fieldItem = new $this->itemClass($item, $this); $fields = $fieldItem->Fields(false); $columnData = array(); if($fields) foreach($fields as $field) { $value = $field->Value; // TODO This should be replaced with casting if(array_key_exists($field->Name, $this->csvFieldFormatting)) { $format = str_replace('$value', "__VAL__", $this->csvFieldFormatting[$field->Name]); $format = preg_replace('/\$([A-Za-z0-9-_]+)/','$item->$1', $format); $format = str_replace('__VAL__', '$value', $format); eval('$value = "' . $format . '";'); } $value = str_replace(array("\r", "\n"), "\n", $value); $tmpColumnData = '"' . str_replace('"', '\"', $value) . '"'; $columnData[] = $tmpColumnData; } $fileData .= implode($separator, $columnData); $fileData .= "\n"; $item->destroy(); unset($item); unset($fieldItem); } $numColumns = count($columnData); $numRows = $fieldItems->count(); return $fileData; } else { return null; } } /** * We need to instanciate this button manually as a normal button has no means of adding inline onclick-behaviour. */ function ExportLink() { $exportLink = Controller::join_links($this->Link(), 'export'); if($this->extraLinkParams) $exportLink .= "?" . http_build_query($this->extraLinkParams); return $exportLink; } function printall() { Requirements::clear(); Requirements::css(CMS_DIR . '/css/typography.css'); Requirements::css(CMS_DIR . '/css/cms_right.css'); Requirements::css(SAPPHIRE_DIR . '/css/TableListField_print.css'); $this->cachedSourceItems = null; $oldShowPagination = $this->showPagination; $this->showPagination = false; increase_time_limit_to(); $this->Print = true; $result = $this->renderWith(array($this->template . '_printable', 'TableListField_printable')); $this->showPagination = $oldShowPagination; return $result; } function PrintLink() { $link = Controller::join_links($this->Link(), 'printall'); if(isset($_REQUEST['ctf'][$this->getName()]['sort'])) { $link = HTTP::setGetVar("ctf[{$this->getName()}][sort]",Convert::raw2xml($_REQUEST['ctf'][$this->getName()]['sort']), $link); } return $link; } /** * ################################# * Utilty * ################################# */ function Utility() { $links = new ArrayList(); if($this->can('export')) { $links->push(new ArrayData(array( 'Title' => _t('TableListField.CSVEXPORT', 'Export to CSV'), 'Link' => $this->ExportLink() ))); } if($this->can('print')) { $links->push(new ArrayData(array( 'Title' => _t('TableListField.PRINT', 'Print'), 'Link' => $this->PrintLink() ))); } return $links; } /** * Returns the content of the TableListField as a piece of FormResponse javascript * @deprecated Please use the standard URL through Link() which gives you the FieldHolder as an HTML fragment. */ function ajax_refresh() { Deprecation::notice('2.4', 'Please use the standard URL through Link() which gives you the FieldHolder as an HTML fragment instead.'); // 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, 1); FormResponse::set_non_ajax_content($response); return FormResponse::respond(); } function setFieldCasting($casting) { $this->fieldCasting = $casting; } function setFieldFormatting($formatting) { $this->fieldFormatting = $formatting; } function setCSVFieldFormatting($formatting) { $this->csvFieldFormatting = $formatting; } /** * Edit the field list */ function setFieldList($fieldList) { $this->fieldList = $fieldList; } /** * @return String */ function Name() { return $this->name; } function Title() { // adding translating functionality // this is a bit complicated, because this parameter is passed to this class // and should come here translated already // adding this to TODO probably add a method to the classes // to return they're translated string // added by ruibarreiros @ 27/11/2007 return $this->sourceClass() ? singleton($this->sourceClass())->singular_name() : $this->getName(); } function NameSingular() { // same as Title() // added by ruibarreiros @ 27/11/2007 return $this->sourceClass() ? singleton($this->sourceClass())->singular_name() : $this->getName(); } function NamePlural() { // same as Title() // added by ruibarreiros @ 27/11/2007 return $this->sourceClass() ? singleton($this->sourceClass())->plural_name() : $this->getName(); } function setTemplate($template) { $this->template = $template; } function CurrentLink() { $link = $this->Link(); if(isset($_REQUEST['ctf'][$this->getName()]['start']) && is_numeric($_REQUEST['ctf'][$this->getName()]['start'])) { $start = ($_REQUEST['ctf'][$this->getName()]['start'] < 0) ? 0 : $_REQUEST['ctf'][$this->getName()]['start']; $link = Controller::join_links($link, "?ctf[{$this->getName()}][start]={$start}"); } if($this->extraLinkParams) $link .= "&" . http_build_query($this->extraLinkParams); return $link; } /** * Overloaded to automatically add security token. * * @param String $action * @return String */ function Link($action = null) { $form = $this->getForm(); if($form) { $token = $form->getSecurityToken(); $parentUrlParts = parse_url(parent::Link($action)); $queryPart = (isset($parentUrlParts['query'])) ? '?' . $parentUrlParts['query'] : null; // Ensure that URL actions not routed through Form->httpSubmission() are protected against CSRF attacks. if($token->isEnabled()) $queryPart = $token->addtoUrl($queryPart); return Controller::join_links($parentUrlParts['path'], $queryPart); } else { // allow for instanciation of this FormField outside of a controller/form // context (e.g. for unit tests) return false; } } function BaseLink() { user_error("TableListField::BaseLink() deprecated, use Link() instead", E_USER_NOTICE); return $this->Link(); } /** * Helper method to determine permissions for a scaffolded * TableListField (or subclasses) - currently used in {@link ModelAdmin} and {@link DataObject->scaffoldFormFields()}. * Returns true for each permission that doesn't have an explicit getter. * * @todo Temporary method, implement directly in FormField subclasses with object-level permissions. * * @param string $class * @param numeric $id * @return array */ public static function permissions_for_object($class, $id = null) { $permissions = array(); $obj = ($id) ? DataObject::get_by_id($class, $id) : singleton($class); if(!$obj->hasMethod('canView') || $obj->canView()) $permissions[] = 'show'; if(!$obj->hasMethod('canEdit') || $obj->canEdit()) $permissions[] = 'edit'; if(!$obj->hasMethod('canDelete') || $obj->canDelete()) $permissions[] = 'delete'; if(!$obj->hasMethod('canCreate') || $obj->canCreate()) $permissions[] = 'add'; return $permissions; } /** * @param $value * */ function getCastedValue($value, $castingDefinition) { if(is_array($castingDefinition)) { $castingParams = $castingDefinition; array_shift($castingParams); $castingDefinition = array_shift($castingDefinition); } else { $castingParams = array(); } if(strpos($castingDefinition,'->') === false) { $castingFieldType = $castingDefinition; $castingField = DBField::create($castingFieldType, $value); $value = call_user_func_array(array($castingField,'XML'),$castingParams); } else { $fieldTypeParts = explode('->', $castingDefinition); $castingFieldType = $fieldTypeParts[0]; $castingMethod = $fieldTypeParts[1]; $castingField = DBField::create($castingFieldType, $value); $value = call_user_func_array(array($castingField,$castingMethod),$castingParams); } return $value; } function setHighlightConditions($conditions) { $this->highlightConditions = $conditions; } /** * See {@link SelectOptions()} for introduction. * * @param $options array Options to add, key being a unique identifier of the action, * and value a title for the rendered link element (can contain HTML). * The keys for 'all' and 'none' have special behaviour associated * through TableListField.js JavaScript. * For any other key, the JavaScript automatically checks all checkboxes contained in * elements with a matching classname. */ function addSelectOptions($options){ foreach($options as $k => $title) $this->selectOptions[$k] = $title; } /** * Remove one all more table's {@link $selectOptions} * * @param $optionsNames array */ function removeSelectOptions($names){ foreach($names as $name){ unset($this->selectOptions[trim($name)]); } } /** * Return the table's {@link $selectOptions}. * Used to toggle checkboxes for each table row through button elements. * * Requires {@link Markable()} to return TRUE. * This is only functional with JavaScript enabled. * * @return SS_List of ArrayData objects */ function SelectOptions(){ if(!$this->selectOptions) return; $selectOptionsSet = new ArrayList(); foreach($this->selectOptions as $k => $v) { $selectOptionsSet->push(new ArrayData(array( 'Key' => $k, 'Value' => $v ))); } return $selectOptionsSet; } } /** * A single record in a TableListField. * @package forms * @subpackage fields-relational * @see TableListField */ class TableListField_Item extends ViewableData { /** * @var DataObject The underlying data record, * usually an element of {@link TableListField->sourceItems()}. */ protected $item; /** * @var TableListField */ protected $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($xmlSafe = true) { $list = $this->parent->FieldList(); foreach($list as $fieldName => $fieldTitle) { $value = ""; // This supports simple FieldName syntax if(strpos($fieldName,'.') === false) { $value = ($this->item->XML_val($fieldName) && $xmlSafe) ? $this->item->XML_val($fieldName) : $this->item->RAW_val($fieldName); // This support the syntax fieldName = Relation.RelatedField } else { $fieldNameParts = explode('.', $fieldName) ; $tmpItem = $this->item; for($j=0;$j$relationMethod; } else { if($tmpItem) $tmpItem = $tmpItem->$relationMethod(); } } } // casting if(array_key_exists($fieldName, $this->parent->fieldCasting)) { $value = $this->parent->getCastedValue($value, $this->parent->fieldCasting[$fieldName]); } elseif(is_object($value) && method_exists($value, 'Nice')) { $value = $value->Nice(); } // 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 . '";'); } //escape if($escape = $this->parent->fieldEscape){ foreach($escape as $search => $replace){ $value = str_replace($search, $replace, $value); } } $fields[] = new ArrayData(array( "Name" => $fieldName, "Title" => $fieldTitle, "Value" => $value, "CsvSeparator" => $this->parent->getCsvSeparator(), )); } return new ArrayList($fields); } function Markable() { return $this->parent->Markable; } /** * Checks global permissions for field in {@link TableListField->Can()}. * If they are allowed, it checks for object permissions by assuming * a method with "can" + $mode parameter naming, e.g. canDelete(). * * @param string $mode See {@link TableListField::$permissions} array. * @return boolean */ function Can($mode) { $canMethod = "can" . ucfirst($mode); if(!$this->parent->Can($mode)) { // check global settings for the field instance return false; } elseif($this->item->hasMethod($canMethod)) { // if global allows, check object specific permissions (e.g. canDelete()) return $this->item->$canMethod(); } else { // otherwise global allowed this action, so return TRUE return true; } } function Link($action = null) { $form = $this->parent->getForm(); if($form) { $token = $form->getSecurityToken(); $parentUrlParts = parse_url($this->parent->Link()); $queryPart = (isset($parentUrlParts['query'])) ? '?' . $parentUrlParts['query'] : null; // Ensure that URL actions not routed through Form->httpSubmission() are protected against CSRF attacks. if($token->isEnabled()) $queryPart = $token->addtoUrl($queryPart); return Controller::join_links($parentUrlParts['path'], 'item', $this->item->ID, $action, $queryPart); } else { // allow for instanciation of this FormField outside of a controller/form // context (e.g. for unit tests) return false; } } /** * Returns all row-based actions not disallowed through permissions. * See TableListField->Action for a similiar dummy-function to work * around template-inheritance issues. * * @return SS_List */ function Actions() { $allowedActions = new ArrayList(); foreach($this->parent->actions as $actionName => $actionSettings) { if($this->parent->Can($actionName)) { $allowedActions->push(new ArrayData(array( 'Name' => $actionName, 'Link' => $this->{ucfirst($actionName).'Link'}(), 'Icon' => $actionSettings['icon'], 'IconDisabled' => $actionSettings['icon_disabled'], 'Label' => $actionSettings['label'], 'Class' => $actionSettings['class'], 'Default' => ($actionName == $this->parent->defaultAction), 'IsAllowed' => $this->Can($actionName), ))); } } return $allowedActions; } function BaseLink() { user_error("TableListField_Item::BaseLink() deprecated, use Link() instead", E_USER_NOTICE); return $this->Link(); } function DeleteLink() { return Controller::join_links($this->Link(), "delete"); } function MarkingCheckbox() { $name = $this->parent->getName() . '[]'; if($this->parent->isReadonly()) return "item->ID}\" disabled=\"disabled\" />"; else return "item->ID}\" />"; } /** * According to {@link TableListField->selectOptions}, each record will check if the options' key on the object is true, * if it is true, add the key as a class to the record * * @return string Value for a 'class' HTML attribute. */ function SelectOptionClasses(){ $tagArray = array('markingcheckbox'); $options = $this->parent->SelectOptions(); if($options && $options->exists()){ foreach($options as $option){ if($option->Key !== 'all' && $option->Key !== 'none'){ if($this->{$option->Key}) { $tagArray[] = $option->Key; } } } } return implode(" ",$tagArray); } 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(isset($condition['exclusive']) && $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'); } } /** * @package forms * @subpackage fields-relational */ class TableListField_ItemRequest extends RequestHandler { protected $ctf; protected $itemID; protected $methodName; static $url_handlers = array( '$Action!' => '$Action', '' => 'index', ); function Link() { return Controller::join_links($this->ctf->Link(), 'item/' . $this->itemID); } function __construct($ctf, $itemID) { $this->ctf = $ctf; $this->itemID = $itemID; parent::__construct(); } function delete($request) { // Protect against CSRF on destructive action $token = $this->ctf->getForm()->getSecurityToken(); if(!$token->checkRequest($request)) return $this->httpError('400'); if($this->ctf->Can('delete') !== true) { return false; } $this->dataObj()->delete(); } /////////////////////////////////////////////////////////////////////////////////////////////////// /** * Return the data object being manipulated */ function dataObj() { // used to discover fields if requested and for population of field if(is_numeric($this->itemID)) { // we have to use the basedataclass, otherwise we might exclude other subclasses return $this->ctf->getDataList()->byId($this->itemID); } } /** * @return TableListField */ function getParentController() { return $this->ctf; } } ?>