diff --git a/css/GridField.css b/css/GridField.css index f8644546e..bfbd670fb 100644 --- a/css/GridField.css +++ b/css/GridField.css @@ -1,17 +1,32 @@ -/** Core styles for the basic GridField form field without any specific style. @package sapphire @subpackage scss */ -.ss-gridfield { border: none; } -.ss-gridfield table { width: 100%; border-collapse: collapse; border-spacing: 0; background: #fff; border: 1px solid #c1c1c1; } -.ss-gridfield thead { color: #5a5a5a; background: #dadada; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #f3f3f3), color-stop(100%, #dadada)); background-image: -webkit-linear-gradient(#f3f3f3, #dadada); background-image: -moz-linear-gradient(#f3f3f3, #dadada); background-image: -o-linear-gradient(#f3f3f3, #dadada); background-image: -ms-linear-gradient(#f3f3f3, #dadada); background-image: linear-gradient(#f3f3f3, #dadada); } -.ss-gridfield thead th { font-weight: bold; padding: 8px 24px 8px 8px; position: relative; border: 1px solid #c1c1c1; border-width: 0 1px 1px 0; } -.ss-gridfield thead th.ss-gridfield-sortable.hover { color: #747474; cursor: pointer; background: #f3f3f3; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #e7e7e7)); background-image: -webkit-linear-gradient(#ffffff, #e7e7e7); background-image: -moz-linear-gradient(#ffffff, #e7e7e7); background-image: -o-linear-gradient(#ffffff, #e7e7e7); background-image: -ms-linear-gradient(#ffffff, #e7e7e7); background-image: linear-gradient(#ffffff, #e7e7e7); } -.ss-gridfield thead th.ss-gridfield-sorted { background: #e7e7e7; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #dadada), color-stop(100%, #f3f3f3)); background-image: -webkit-linear-gradient(#dadada, #f3f3f3); background-image: -moz-linear-gradient(#dadada, #f3f3f3); background-image: -o-linear-gradient(#dadada, #f3f3f3); background-image: -ms-linear-gradient(#dadada, #f3f3f3); background-image: linear-gradient(#dadada, #f3f3f3); } -.ss-gridfield thead th.ss-gridfield-sorted.hover { background: #f3f3f3; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #e7e7e7), color-stop(100%, #f9f9f9)); background-image: -webkit-linear-gradient(#e7e7e7, #f9f9f9); background-image: -moz-linear-gradient(#e7e7e7, #f9f9f9); background-image: -o-linear-gradient(#e7e7e7, #f9f9f9); background-image: -ms-linear-gradient(#e7e7e7, #f9f9f9); background-image: linear-gradient(#e7e7e7, #f9f9f9); } -.ss-gridfield thead th .ui-icon { position: absolute; top: 5px; right: 0; } -.ss-gridfield thead th.ss-gridfield-desc .ui-icon { background-position: 0 -48px; } -.ss-gridfield thead th.ss-gridfield-asc .ui-icon { background-position: -64px -48px; } -.ss-gridfield td { padding: 8px; border-right: 1px solid #f3f3f3; } -.ss-gridfield td.ss-gridfield-last { border-right: none; } -.ss-gridfield tr.ss-gridfield-even { border: 1px solid #c6e5f6; border-width: 1px 0; background: #f2f9fd; } -.ss-gridfield tr.ss-gridfield-even.ss-gridfield-last { border-bottom: none; } -.ss-gridfield tr.ss-gridfield-even td { border-right: 1px solid #dceffa; } -.ss-gridfield tr.ss-gridfield-even td.ss-gridfield-last { border-right: none; } +/** Core styles for the basic GridField form field without any specific style. @package sapphire @subpackage scss @todo Add radial gradient to default delete button state @todo Create SASS mixin-function to simply swap the from/to, to to/from colours in grsdient mixins? */ +.cms table.ss-gridfield { width: 100%; padding: 0; margin: 20px 0 0 0; border-collapse: separate; display: table; border-bottom: 0 none; } +.cms table.ss-gridfield thead { color: #1d2224; background: transparent; } +.cms table.ss-gridfield tbody { background: #FFF; } +.cms table.ss-gridfield tbody td { /* Rounded buttons */ } +.cms table.ss-gridfield tbody td button { border: #CC0033 solid 1px; background: #CC0033; color: #FFF; -moz-border-radius: 15px; -webkit-border-radius: 15px; -o-border-radius: 15px; -ms-border-radius: 15px; -khtml-border-radius: 15px; border-radius: 15px; margin: 0 0 0 2px; padding: 0; width: auto; min-width: 30px; height: 30px; text-shadow: none; } +.cms table.ss-gridfield tbody td button:hover { color: #222; } +.cms table.ss-gridfield tfoot { color: #1d2224; } +.cms table.ss-gridfield tfoot tr td { background: #95a5ab; padding: .7em; } +.cms table.ss-gridfield tr.sortable-header th { background: #7f9198; } +.cms table.ss-gridfield tr:hover { background: #FFFAD6 !important; } +.cms table.ss-gridfield tr:first-child { background: transparent; } +.cms table.ss-gridfield tr.ss-gridfield-even { background: #f2f9fd; } +.cms table.ss-gridfield tr.ss-gridfield-even.ss-gridfield-last { border-bottom: none; } +.cms table.ss-gridfield tr th { font-weight: bold; font-size: 12px; color: #FFF; padding: 0; border-right: 1px solid #85959C; height: 20px; /* Makes it appear as though the text sits over the boundary of the two 's in */ } +.cms table.ss-gridfield tr th span { display: block; position: relative; left: 20px; top: -7px; width: 100%; } +.cms table.ss-gridfield tr th div.fieldgroup, .cms table.ss-gridfield tr th div.fieldgroup-field { width: auto; } +.cms table.ss-gridfield tr th div.fieldgroup { min-width: 200px; /* Not sure why IE think it needs this */ } +.cms table.ss-gridfield tr th.extra, .cms table.ss-gridfield tr th.action { background: #7f9198; padding: 0; cursor: default; } +.cms table.ss-gridfield tr th.extra button.ss-ui-button, .cms table.ss-gridfield tr th.extra button:hover.ss-ui-button, .cms table.ss-gridfield tr th.action button.ss-ui-button, .cms table.ss-gridfield tr th.action button:hover.ss-ui-button { margin-left: .9em; color: #222; } +.cms table.ss-gridfield tr th.extra { text-align: center; background: #b1c0c5; background: -moz-linear-gradient(#b1c0c5 20%, #7f9198); /* FF3.6+ */ background: -webkit-gradient(linear, left top, left bottom, color-stop(20%, #b1c0c5), color-stop(80%, #7f9198)); /* Chrome,Safari4+ */ background: -webkit-linear-gradient(top, #b1c0c5 20%, #7f9198 80%); /* Chrome10+,Safari5.1+ */ background: -o-linear-gradient(top, #b1c0c5 20%, #7f9198 80%); /* Opera 11.10+ */ filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,startColorstr='#b1c0c5', endColorstr='#7f9198'); /* IE5.5+ */ background: -ms-linear-gradient(top, #b1c0c5 20%, #7f9198 80%); /* IE10+ */ background: linear-gradient(top, #b1c0c5 20%, #7f9198 80%); /* W3C */ } +.cms table.ss-gridfield tr th.extra span { width: auto; display: inline; position: static; } +.cms table.ss-gridfield tr th.action { border-right: 0; } +.cms table.ss-gridfield tr th.first { -moz-border-radius-topleft: 7px; -webkit-border-top-left-radius: 7px; -o-border-top-left-radius: 7px; -ms-border-top-left-radius: 7px; -khtml-border-top-left-radius: 7px; border-top-left-radius: 7px; } +.cms table.ss-gridfield tr th.last { -moz-border-radius-topright: 7px; -webkit-border-top-right-radius: 7px; -o-border-top-right-radius: 7px; -ms-border-top-right-radius: 7px; -khtml-border-top-right-radius: 7px; border-top-right-radius: 7px; } +.cms table.ss-gridfield tr th button, .cms table.ss-gridfield tr th button:hover { font-size: 12px; margin-left: -0.9em; } +.cms table.ss-gridfield tr th button.ss-gridfield-sort, .cms table.ss-gridfield tr th button:hover.ss-gridfield-sort { text-align: left; padding: 0; color: #FFF; width: 95%; background: transparent; border: 0 none; box-shadow: none; text-shadow: none; } +.cms table.ss-gridfield tr th button:hover { color: #CCC !important; /* Not sure why IE think it needs this */ } +.cms table.ss-gridfield tr th.extra button.ss-ui-button { padding: .3em; line-height: 1; box-shadow: none; position: relative; top: -24px; border: #b1c0c5 solid 10px; border-bottom-width: 0; } +.cms table.ss-gridfield tr th input.ss-gridfield-sort { position: relative; top: -24px; padding: 2px; width: 65%; margin: 0 auto; border: #b1c0c5 solid 10px; border-bottom: 0; } +.cms table.ss-gridfield tr td { border-right: 1px solid #dbdddd; padding: 10px; } +.cms table.ss-gridfield tr td.bottom-all { -moz-border-radius-bottomleft: 7px; -webkit-border-bottom-left-radius: 7px; -o-border-bottom-left-radius: 7px; -ms-border-bottom-left-radius: 7px; -khtml-border-bottom-left-radius: 7px; border-bottom-left-radius: 7px; -moz-border-radius-bottomright: 7px; -webkit-border-bottom-right-radius: 7px; -o-border-bottom-right-radius: 7px; -ms-border-bottom-right-radius: 7px; -khtml-border-bottom-right-radius: 7px; border-bottom-right-radius: 7px; background: #7f9198; background: -moz-linear-gradient(#7f9198 20%, #b1c0c5); /* FF3.6+ */ background: -webkit-gradient(linear, left top, left bottom, color-stop(20%, #7f9198), color-stop(80%, #b1c0c5)); /* Chrome,Safari4+ */ background: -webkit-linear-gradient(top, #7f9198 20%, #b1c0c5 80%); /* Chrome10+,Safari5.1+ */ background: -o-linear-gradient(top, #7f9198 20%, #b1c0c5 80%); /* Opera 11.10+ */ filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,startColorstr='#7f9198', endColorstr='#b1c0c5'); /* IE5.5+ */ background: -ms-linear-gradient(top, #7f9198 20%, #7f9198 80%); /* IE10+ */ background: linear-gradient(top, #7f9198 20%, #b1c0c5 80%); /* W3C */ } diff --git a/filesystem/Folder.php b/filesystem/Folder.php index 949e5ecef..0323b59e0 100644 --- a/filesystem/Folder.php +++ b/filesystem/Folder.php @@ -25,12 +25,26 @@ class Folder extends File { static $default_sort = "\"Sort\""; - function populateDefaults() { + /** + * + */ + public function populateDefaults() { parent::populateDefaults(); if(!$this->Name) $this->Name = _t('AssetAdmin.NEWFOLDER',"NewFolder"); } + /** + * @param $folderPath string Absolute or relative path to the file. + * If path is relative, its interpreted relative to the "assets/" directory. + * @return Folder + * @deprecated in favor of the correct name find_or_make + */ + public static function findOrMake($folderPath) { + Deprecation::notice('3.0', "Folder::findOrMake() is deprecated in favor of Folder::find_or_make()"); + return self::find_or_make($folderPath); + } + /** * Find the given folder or create it both as {@link Folder} database records * and on the filesystem. If necessary, creates parent folders as well. @@ -39,7 +53,7 @@ class Folder extends File { * If path is relative, its interpreted relative to the "assets/" directory. * @return Folder */ - static function findOrMake($folderPath) { + public static function find_or_make($folderPath) { // Create assets directory, if it is missing if(!file_exists(ASSETS_PATH)) Filesystem::makeFolder(ASSETS_PATH); diff --git a/filesystem/Upload.php b/filesystem/Upload.php index e042dc5dc..fdfeec3ce 100644 --- a/filesystem/Upload.php +++ b/filesystem/Upload.php @@ -120,7 +120,7 @@ class Upload extends Controller { // @TODO This puts a HUGE limitation on files especially when lots // have been uploaded. $base = Director::baseFolder(); - $parentFolder = Folder::findOrMake($folderPath); + $parentFolder = Folder::find_or_make($folderPath); // Create a folder for uploading. if(!file_exists(ASSETS_PATH)){ diff --git a/forms/Form.php b/forms/Form.php index 5e717c1a6..b602ad03f 100644 --- a/forms/Form.php +++ b/forms/Form.php @@ -302,6 +302,23 @@ class Form extends RequestHandler { sprintf('Action "%s" not allowed on form (Name: "%s")', $funcName, $this->Name()) ); } + // TODO : Once we switch to a stricter policy regarding allowed_actions (meaning actions must be set explicitly in allowed_actions in order to run) + // Uncomment the following for checking security against running actions on form fields + /* else { + // Try to find a field that has the action, and allows it + $fieldsHaveMethod = false; + foreach ($this->Fields() as $field){ + if ($field->hasMethod($funcName) && $field->checkAccessAction($funcName)) { + $fieldsHaveMethod = true; + } + } + if (!$fieldsHaveMethod) { + return $this->httpError( + 403, + sprintf('Action "%s" not allowed on any fields of form (Name: "%s")', $funcName, $this->Name()) + ); + } + }*/ // Validate the form if(!$this->validate()) { @@ -344,6 +361,12 @@ class Form extends RequestHandler { // Otherwise, try a handler method on the form object. } elseif($this->hasMethod($funcName)) { return $this->$funcName($vars, $this, $request); + } else { + // Finally try to find a field that could handle that action, ie GridField + foreach ($this->Fields() as $field){ + if (!$field->hasMethod($funcName)) continue; + return $field->$funcName($vars, $this, $request); + } } return $this->httpError(404); diff --git a/forms/GridField.php b/forms/GridField.php deleted file mode 100644 index ba64b36e3..000000000 --- a/forms/GridField.php +++ /dev/null @@ -1,174 +0,0 @@ - - * $gridField = new GridField('ExampleGrid', 'Example grid', new DataList('Page')); - * - * - * If you want to modify the output of the grid you can attach a customised - * DataGridPresenter that are the actual Renderer of the data. Sapphire provides - * a default one if you chooses not to. - * - * @see GridFieldPresenter - * @see SS_List - * - * @package sapphire - * @subpackage forms - */ -class GridField extends FormField { - - /** - * @var SS_List - */ - protected $list = null; - - /** - * @var string - */ - protected $presenterClassName = "GridFieldPresenter"; - - /** - * @var GridFieldPresenter - */ - protected $presenter = null; - - /** - * @var string - the classname of the DataObject that the GridField will display - */ - protected $modelClassName = ''; - - /** - * Url handlers - * - * @var array - */ - public static $url_handlers = array( - '$Action' => '$Action', - ); - - /** - * Creates a new GridField field - * - * @param string $name - * @param string $title - * @param SS_List $dataList - * @param Form $form - * @param string|GridFieldPresenter $dataPresenterClassName - can either pass in a string or an instance of a GridFieldPresenter - */ - public function __construct($name, $title = null, SS_List $dataList = null, Form $form = null, $dataPresenterClassName = 'GridFieldPresenter') { - parent::__construct($name, $title, null, $form); - - if ($dataList) { - $this->setList($dataList); - } - - $this->setPresenter($dataPresenterClassName); - } - - /** - * - * @return string - HTML - */ - public function index() { - return $this->FieldHolder(); - } - - /** - * @param string $modelClassName - */ - public function setModelClass($modelClassName) { - $this->modelClassName = $modelClassName; - - return $this; - } - - /** - * @throws Exception - * @return string - */ - public function getModelClass() { - if ($this->modelClassName) { - return $this->modelClassName; - } - if ($this->list->dataClass) { - return $this->list->dataClass; - } - - throw new LogicException(get_class($this).' does not have a modelClassName'); - } - - /** - * @param string|GridFieldPresenter - * - * @throws Exception - */ - public function setPresenter($presenter) { - if(!$presenter){ - throw new InvalidArgumentException('setPresenter() for GridField must be set with a class'); - } - - if(is_object($presenter)) { - $this->presenter = $presenter; - $this->presenter->setGridField($this); - - return; - } - - if(!class_exists($presenter)){ - throw new InvalidArgumentException('DataPresenter for GridField must be set with an existing class, '.$presenter.' does not exists.'); - } - - if($presenter !='GridFieldPresenter' && !is_subclass_of($presenter, 'GridFieldPresenter')) { - throw new InvalidArgumentException(sprintf( - 'DataPresenter "%s" must subclass GridFieldPresenter', $presenter - )); - } - - $this->presenter = new $presenter; - $this->presenter->setGridField($this); - - return $this; - } - - /** - * @return GridFieldPresenter - */ - public function getPresenter(){ - return $this->presenter; - } - - /** - * Set the datasource - * - * @param SS_List $list - */ - public function setList(SS_List $list) { - $this->list = $list; - return $this; - } - - /** - * Get the datasource - * - * @return SS_List - */ - public function getList() { - return $this->list; - } - - /** - * @return string - html for the form - */ - function FieldHolder() { - return $this->getPresenter()->render(); - } -} - \ No newline at end of file diff --git a/forms/GridFieldPaginator.php b/forms/GridFieldPaginator.php deleted file mode 100644 index e847a2da4..000000000 --- a/forms/GridFieldPaginator.php +++ /dev/null @@ -1,277 +0,0 @@ -totalNumberOfPages = $totalNumberOfPages; - $this->currentPage = $currentPage; - } - - /** - * Returns the rendered template for GridField - * - * @return string - */ - public function Render() { - return $this->renderWith(array($this->template)); - } - - /** - * Returns a url to the last page in the result - * - * @return string - */ - public function FirstLink() { - if($this->haveNoPages()) { - return false; - } - return 1; - } - - /** - * Returns a url to the previous page in the result - * - * @return string - */ - public function PreviousLink() { - if($this->isFirstPage() || $this->haveNoPages()) { - return false; - } - // Out of bounds - if($this->currentPage>$this->totalNumberOfPages){ - return $this->LastLink(); - } - - return ($this->currentPage-1); - } - - /** - * Returns a list of pages with links, pagenumber and if it is the current - * page. - * - * @return ArrayList - */ - public function Pages() { - if($this->haveNoPages()) { - return false; - } - - $list = new ArrayList(); - for($idx=1;$idx<=$this->totalNumberOfPages;$idx++) { - $data = new ArrayData(array()); - $data->setField('PageNumber',$idx); - if($idx == $this->currentPage ) { - $data->setField('Current',true); - } else { - $data->setField('Current',false); - } - - $data->setField('Link',$idx); - $list->push($data); - } - return $list; - } - - /** - * Returns a url to the next page in the result - * - * @return string - */ - public function NextLink() { - if($this->isLastPage() || $this->haveNoPages() ) { - return false; - } - // Out of bounds - if($this->currentPage<1) { - return $this->FirstLink(); - } - return ($this->currentPage+1); - } - - /** - * Returns a url to the last page in the result - * - * @return string - */ - public function LastLink() { - if($this->haveNoPages()) { - return false; - } - return ($this->totalNumberOfPages); - } - - /** - * Are we currently on the first page - * - * @return bool - */ - protected function isFirstPage() { - return (bool)($this->currentPage<=1); - } - - /** - * Are we currently on the last page? - * - * @return bool - */ - protected function isLastPage() { - return (bool)($this->currentPage>=$this->totalNumberOfPages); - } - - /** - * Is there only one page of results? - * - * @return bool - */ - protected function haveNoPages() { - return (bool)($this->totalNumberOfPages<=1); - } - -} - -/** - * This is the extension that decorates the GridFieldPresenter. Since a extension - * can't be a Viewable data it's split like this. - * - * @see GridField - * @package sapphire - */ -class GridFieldPaginator_Extension extends Extension { - - /** - * - * @var int - */ - protected $paginationLimit; - - /** - * - * @var int - */ - protected $totalNumberOfPages = 1; - - /** - * - * @var int - */ - protected $currentPage = 1; - - /** - * - * @return string - */ - public function Footer() { - return new GridFieldPaginator($this->totalNumberOfPages, $this->currentPage); - } - - /** - * NOP - */ - public function __construct() {} - - /** - * Set the limit for each page - * - * @param int $limit - * @return GridFieldPaginator_Extension - */ - public function paginationLimit($limit) { - $this->paginationLimit = $limit; - return $this; - } - - /** - * Filter the list to only contain a pagelength of items - * - * @return bool - if the pagination was activated - * @see GridFieldPresenter::Items() - */ - public function filterList(SS_List $list, $parameters){ - if(!$this->canUsePagination($list)) { - return false; - } - - $currentPage = $parameters->Request->requestVar('page'); - if(!$currentPage) { - $currentPage = 1; - } - - $this->totalNumberOfPages = $this->getMaxPagesCount($list); - - if($currentPage<1) { - // Current page is below 1, show nothing and save cpu cycles - $list->where('1=0'); - } elseif($currentPage > $this->totalNumberOfPages) { - // current page is over max pages, show nothing and save cpu cycles - $list->where('1=0'); - } else { - $offset = ($currentPage-1)*$this->paginationLimit; - $list->getRange((int)$offset,$this->paginationLimit); - } - $this->currentPage = $currentPage; - - return true; - } - - /** - * Helper function that see if the pagination has been set and that the - * $list can use pagination. - * - * @param SS_List $list - * @return bool - */ - protected function canUsePagination(SS_List $list) { - if(!$this->paginationLimit) { - return false; - } - if(!method_exists($list, 'getRange')) { - return false; - } - if(!method_exists($list, 'limit')){ - return false; - } - return true; - } - - /** - * - * @return int - */ - protected function getMaxPagesCount($list) { - $list->limit(null); - $number = $list->count(); - $number = ceil($number/$this->paginationLimit); - return $number; - } -} \ No newline at end of file diff --git a/forms/GridFieldPresenter.php b/forms/GridFieldPresenter.php deleted file mode 100644 index a44623a88..000000000 --- a/forms/GridFieldPresenter.php +++ /dev/null @@ -1,417 +0,0 @@ - - * $presenter = new GridFieldPresenter(); - * $presenter->sort('Title', 'desc'); - * $gridField = new GridField('ExampleGrid', 'Example grid', new DataList('Page'),null, $presenter); - * - * - * Another example is to change the template for the rendering - * - * - * $presenter = new GridFieldPresenter(); - * $presenter->setTemplate('MyNiftyGridTemplate'); - * $gridField = new GridField('ExampleGrid', 'Example grid', new DataList('Page'),null, $presenter); - * - * - * There is also a possibility to add extensions to the GridPresenter. An - * example is the DataGridPagination that decorates the GridField with - * pagination. Look in the GridFieldPresenter::Items() and the filterList extend - * and GridFieldPresenter::Footers() - * - * - * GridFieldPresenter::add_extension('GridFieldPaginator_Extension'); - * $presenter = new GridFieldPresenter(); - * // This is actually calling GridFieldPaginator_Extension::paginationLimit() - * $presenter->paginationLimit(3); - * $gridField = new GridField('ExampleGrid', 'Example grid', new DataList('Page'),null, $presenter); - * - * - * @see GridField - * @see GridFieldPaginator - * @package sapphire - */ -class GridFieldPresenter extends ViewableData { - - /** - * Template override - * - * @var string $template - */ - protected $template = 'GridFieldPresenter'; - - /** - * Class name for each item/row - * - * @var string $itemClass - */ - protected $itemClass = 'GridFieldPresenter_Item'; - - /** - * @var GridField - */ - protected $GridField = null; - - /** - * @var array - */ - public $fieldCasting = array(); - - /** - * @var array - */ - public $fieldFormatting = array(); - - /** - * List of columns and direction that the {@link GridFieldPresenter} is - * sorted in. - * - * @var array - */ - protected $sorting = array(); - - /** - * @param string $template - */ - public function setTemplate($template){ - $this->template = $template; - } - - /** - * The name of the Field - * - * @return string - */ - public function getName() { - return $this->getGridField()->getName(); - } - - /** - * @param GridField $GridField - */ - public function setGridField(GridField $grid){ - $this->GridField = $grid; - } - - /** - * @return GridField - */ - public function getGridField(){ - return $this->GridField; - } - - /** - * - * @param type $extension - */ - public static function add_extension($extension) { - parent::add_extension(__CLASS__, $extension); - } - - /** - * Sort the grid by columns - * - * @param string $column - * @param string $direction - */ - public function sort($column, $direction = 'asc') { - $this->sorting[$column] = $direction; - - return $this; - } - - /** - * Return an {@link ArrayList} of {@link GridField_Item} objects, suitable for display in the template. - * - * @return ArrayList - */ - public function Items() { - $items = new ArrayList(); - - if($this->sorting) { - $this->setSortingOnList($this->sorting); - } - //empty for now - $list = $this->getGridField()->getList(); - - $parameters = new stdClass(); - $parameters->Controller = Controller::curr(); - $parameters->Request = Controller::curr()->getRequest(); - - $this->extend('filterList', $list, $parameters); - - if($list) { - $numberOfRows = $list->count(); - $counter = 0; - foreach($list as $item) { - $itemPresenter = new $this->itemClass($item, $this); - $itemPresenter->iteratorProperties($counter++, $numberOfRows); - $items->push($itemPresenter); - } - } - return $items; - } - - /** - * Get the headers or column names for this grid - * - * The returning array will have the format of - * - * - * array( - * 'FirstName' => 'First name', - * 'Description' => 'A nice description' - * ) - * - * - * @return ArrayList - * @throws Exception - */ - public function Headers() { - if(!$this->getList()) { - throw new LogicException(sprintf( - '%s needs an data source to be able to render the form', get_class($this->getGridField()) - )); - } - return $this->summaryFieldsToList($this->FieldList()); - } - - /** - * - * @return ArrayList - */ - public function Footers() { - $arrayList = new ArrayList(); - $footers = $this->extend('Footer'); - foreach($footers as $footer) { - $arrayList->push($footer); - } - return $arrayList; - } - - /** - * @return SS_List - */ - public function getList() { - return $this->getGridField()->getList(); - } - - /** - * @return string - name of model - */ - protected function getModelClass() { - return $this->getGridField()->getModelClass(); - } - - /** - * Add the combined sorting on the datasource - * - * If the sorting isn't set in the datasource, only the latest sort - * will be executed. - * - * @param array $sortColumns - */ - protected function setSortingOnList(array $sortColumns) { - $resultColumns = array(); - - foreach($sortColumns as $column => $sortOrder) { - $resultColumns[] = sprintf("%s %s", $column ,$sortOrder); - } - - $sort = implode(', ', $resultColumns); - $this->getList()->sort($sort); - } - - /** - * @return array - */ - public function FieldList() { - return singleton($this->getModelClass())->summaryFields(); - } - - /** - * Translate the summaryFields from a model into a format that is understood - * by the Form renderer - * - * @param array $summaryFields - * - * @return ArrayList - */ - protected function summaryFieldsToList($summaryFields) { - $headers = new ArrayList(); - - if(is_array($summaryFields)) { - $counter = 0; - - foreach ($summaryFields as $name => $title) { - $data = array( - 'Name' => $name, - 'Title' => $title, - 'IsSortable' => true, - 'IsSorted' => false, - 'SortedDirection' => 'asc' - ); - - if(array_key_exists($name, $this->sorting)) { - $data['IsSorted'] = true; - $data['SortedDirection'] = $this->sorting[$name]; - } - - $result = new ArrayData($data); - $result->iteratorProperties($counter++, count($summaryFields)); - - $headers->push($result); - } - } - - return $headers; - } - - /** - * @param array $casting - */ - function setFieldCasting($casting) { - $this->fieldCasting = $casting; - } - - /** - * - * @param type $formatting - */ - function setFieldFormatting($formatting) { - $this->fieldFormatting = $formatting; - } - - /** - * @return string - html - */ - function render(){ - return $this->renderWith(array($this->template)); - } -} - -/** - * A single record in a GridField. - * - * @package sapphire - * @see GridField - */ -class GridFieldPresenter_Item extends ViewableData { - - /** - * @var Object The underlying record, usually an element of - * {@link GridField->datasource()}. - */ - protected $item; - - /** - * @var GridFieldPresenter - */ - protected $parent; - - /** - * @param Object $item - * @param GridFieldPresenter $parent - */ - public function __construct($item, $parent) { - $this->failover = $this->item = $item; - $this->parent = $parent; - - parent::__construct(); - } - - /** - * @return int - */ - public function ID() { - return $this->item->ID; - } - - /** - * @return type - */ - public function Parent() { - return $this->parent; - } - - - /** - * @param bool $xmlSafe - * - * @return ArrayList - */ - public function Fields($xmlSafe = true) { - $list = $this->parent->FieldList(); - $counter = 0; - - foreach($list as $fieldName => $fieldTitle) { - $value = ""; - - // TODO Delegates that to DataList - // 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->getGridField()->fieldEscape){ - foreach($escape as $search => $replace){ - $value = str_replace($search, $replace, $value); - } - } - - $arrayData = new ArrayData(array( - "Name" => $fieldName, - "Title" => $fieldTitle, - "Value" => $value - )); - $arrayData->iteratorProperties($counter++, count($list)); - $fields[] = $arrayData; - } - - return new ArrayList($fields); - } -} diff --git a/forms/gridfield/GridField.php b/forms/gridfield/GridField.php new file mode 100644 index 000000000..160798caa --- /dev/null +++ b/forms/gridfield/GridField.php @@ -0,0 +1,589 @@ + + * $gridField = new GridField('ExampleGrid', 'Example grid', new DataList('Page')); + * + * + * @see SS_List + * + * @package sapphire + * @subpackage fields-relational + */ +class GridField extends FormField { + + /** + * + * @var array + */ + public static $allowed_actions = array( + 'gridFieldAlterAction' + ); + + /** @var SS_List - the datasource */ + protected $list = null; + + /** @var string - the classname of the DataObject that the GridField will display. Defaults to the value of $this->list->dataClass */ + protected $modelClassName = ''; + + /** @var array */ + public $fieldCasting = array(); + + /** @var array */ + public $fieldFormatting = array(); + + /** @var GridState - the current state of the GridField */ + protected $state = null; + + /** + * + * @var GridFieldConfig + */ + protected $config = null; + + /** + * The components list + */ + protected $components = array(); + + /** + * This is the columns that will be visible + * + * @var array + */ + protected $displayFields = array(); + + /** + * Internal dispatcher for column handlers. + * Keys are column names and values are GridField_ColumnProvider objects + */ + protected $columnDispatch = null; + + /** + * Creates a new GridField field + * + * @param string $name + * @param string $title + * @param SS_List $dataList + * @param GridFieldConfig $config + */ + public function __construct($name, $title = null, SS_List $dataList = null, GridFieldConfig $config = null) { + parent::__construct($name, $title, null); + + FormField::__construct($name); + + if($dataList) { + $this->setList($dataList); + } + + if(!$config) { + $this->config = $this->getDefaultConfig(); + } else { + $this->config = $config; + } + + $this->setComponents($this->config); + $this->components[] = new GridState_Component(); + $this->state = new GridState($this); + + + $this->addExtraClass('ss-gridfield'); + $this->requireDefaultCSS(); + + } + + /** + * Set the modelClass that this field will get it column headers from + * + * @param string $modelClassName + */ + public function setModelClass($modelClassName) { + $this->modelClassName = $modelClassName; + return $this; + } + + /** + * Returns a dataclass that is a DataObject type that this field should look like. + * + * @throws Exception + * @return string + */ + public function getModelClass() { + if ($this->modelClassName) return $this->modelClassName; + if ($this->list && $this->list->dataClass) return $this->list->dataClass; + + throw new LogicException('GridField doesn\'t have a modelClassName, so it doesn\'t know the columns of this grid.'); + } + + /** + * Set which Components that this GridFields contain by using a GridFieldConfig + * + * @param GridFieldConfig $config + */ + protected function setComponents(GridFieldConfig $config) { + $this->components = $config->getComponents(); + return $this; + } + + /** + * Get a default configuration for this gridfield + * + * @return GridFieldConfig + */ + protected function getDefaultConfig() { + $config = GridFieldConfig::create(); + $config->addComponent(new GridFieldSortableHeader()); + $config->addComponent(new GridFieldFilter()); + $config->addComponent(new GridFieldDefaultColumns()); + $config->addComponent(new GridFieldPaginator()); + return $config; + } + + /** + * Require the default css styling + */ + protected function requireDefaultCSS() { + Requirements::css('sapphire/css/GridField.css'); + } + + /** + * @return array + */ + public function getDisplayFields() { + if(!$this->displayFields) { + return singleton($this->getModelClass())->summaryFields(); + } + return $this->displayFields; + } + + /** + * + * @return GridFieldConfig + */ + public function getConfig() { + return $this->config; + } + + /** + * + * @param array $fields + */ + public function setDisplayFields(array $fields) { + if(!is_array($fields)) { + throw new InvalidArgumentException('Arguments passed to GridField::setDisplayFields() must be an array'); + } + $this->displayFields = $fields; + return $this; + } + + /** + * @param array $casting + */ + public function setFieldCasting($casting) { + $this->fieldCasting = $casting; + return $this; + } + + /** + * @param array $casting + */ + public function getFieldCasting() { + return $this->fieldCasting; + } + + /** + * @param array $casting + */ + public function setFieldFormatting($formatting) { + $this->fieldFormatting = $formatting; + return $this; + } + + /** + * @param array $casting + */ + public function getFieldFormatting() { + return $this->fieldFormatting; + } + + /** + * Taken from TablelistField + * + * @param $value + * + */ + public 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; + } + + /** + * Set the datasource + * + * @param SS_List $list + */ + public function setList(SS_List $list) { + $this->list = $list; + return $this; + } + + /** + * Get the datasource + * + * @return SS_List + */ + public function getList() { + return $this->list; + } + + /** + * Get the current GridState + * + * @return GridState + */ + public function getState($getData=true) { + if(!$this->state) { + throw new LogicException('State has not been defined'); + } + if($getData) { + return $this->state->getData(); + } + return $this->state; + } + + /** + * Returns the whole gridfield rendered with all the attached Elements + * + * @return string + */ + public function FieldHolder() { + // Get columns + $columns = $this->getColumns(); + + // Get data + $list = $this->getList(); + foreach($this->components as $item) { + if($item instanceof GridField_DataManipulator) { + $list = $item->getManipulatedData($this, $list); + } + } + + // Render headers, footers, etc + $content = array( + 'header' => array(), + 'body' => array(), + 'footer' => array(), + 'before' => array(), + 'after' => array(), + ); + + foreach($this->components as $item) { + if($item instanceof GridField_HTMLProvider) { + $fragments = $item->getHTMLFragments($this); + foreach($fragments as $k => $v) { + $content[$k][] = $v; + } + } + } + + foreach($list as $idx => $record) { + $record->iteratorProperties($idx, $list->count()); + $row = ""; + foreach($columns as $column) { + $colContent = $this->getColumnContent($record, $column); + // A return value of null means this columns should be skipped altogether. + if($colContent === null) continue; + $colAttributes = $this->getColumnAttributes($record, $column); + $row .= $this->createTag('td', $colAttributes, $colContent); + } + $row .= ""; + $content['body'][] = $row; + } + + // Turn into the relevant parts of a table + $head = $content['header'] ? $this->createTag('thead', array(), implode("\n", $content['header'])) : ''; + $body = $content['body'] ? $this->createTag('tbody', array(), implode("\n", $content['body'])) : ''; + $foot = $content['footer'] ? $this->createTag('tfoot', array(), implode("\n", $content['footer'])) : ''; + + $attrs = array( + 'id' => isset($this->id) ? $this->id : null, + 'class' => "field CompositeField {$this->extraClass()}" + ); + return + implode("\n", $content['before']) . + $this->createTag('table', $attrs, $head."\n".$foot."\n".$body) . + implode("\n", $content['after']); + } + + function getColumns() { + // Get column list + $columns = array(); + foreach($this->components as $item) { + if($item instanceof GridField_ColumnProvider) { + $item->augmentColumns($this, $columns); + } + } + return $columns; + } + + public function getColumnContent($record, $column) { + // Build the column dispatch + if(!$this->columnDispatch) $this->buildColumnDispatch(); + + $handler = $this->columnDispatch[$column]; + if($handler) { + return $handler->getColumnContent($this, $record, $column); + } else { + throw new InvalidArgumentException("Bad column '$column'"); + } + } + + public function getColumnAttributes($record, $column) { + // Build the column dispatch + if(!$this->columnDispatch) $this->buildColumnDispatch(); + + $handler = $this->columnDispatch[$column]; + if($handler) { + $attrs = $handler->getColumnAttributes($this, $record, $column); + if(is_array($attrs)) return $attrs; + else if($attrs) throw new LogicException("Non-array response from " . get_class($handler) . "::getColumnAttributes()"); + else return array(); + } else { + throw new InvalidArgumentException("Bad column '$column'"); + } + } + + public function getColumnMetadata($column) { + // Build the column dispatch + if(!$this->columnDispatch) $this->buildColumnDispatch(); + + $handler = $this->columnDispatch[$column]; + if($handler) { + $metadata = $handler->getColumnMetadata($this, $column); + if(is_array($metadata)) return $metadata; + else if($metadata) throw new LogicException("Non-array response from " . get_class($handler) . "::getColumnMetadata()"); + else return array(); + } else { + throw new InvalidArgumentException("Bad column '$column'"); + } + } + + public function getColumnCount() { + // Build the column dispatch + if(!$this->columnDispatch) $this->buildColumnDispatch(); + + return count($this->columnDispatch); + + } + protected function buildColumnDispatch() { + $this->columnDispatch = array(); + foreach($this->components as $item) { + if($item instanceof GridField_ColumnProvider) { + $columns = $item->getColumnsHandled($this); + foreach($columns as $column) { + $this->columnDispatch[$column] = $item; + } + } + } + } + + /** + * This is the action that gets executed when a GridField_AlterAction gets clicked. + * + * @param array $data + * @return string + */ + public function gridFieldAlterAction($data, $form, $request) { + $id = $data['StateID']; + $stateChange = Session::get($id); + + $state = $this->getState(false); + $state->setValue($data['GridState']); + + $gridName = $stateChange['grid']; + $grid = $form->Fields()->fieldByName($gridName); + $actionName = $stateChange['actionName']; + + $args = $stateChange['args']; + $grid->handleAction($actionName, $args, $data); + + // Make the form re-load it's values from the Session after redirect + // so the changes we just made above survive the page reload + // @todo Form really needs refactoring so we dont have to do this + if (Director::is_ajax()) { + return $form->forTemplate(); + } else { + $data = $form->getData(); + Session::set("FormInfo.{$form->FormName()}.errors", array()); + Session::set("FormInfo.{$form->FormName()}.data", $data); + Controller::curr()->redirectBack(); + } + + } + + public function handleAction($actionName, $args, $data) { + $actionName = strtolower($actionName); + foreach($this->components as $item) { + if(!($item instanceof GridField_ActionProvider)) { + continue; + } + + if(in_array($actionName, array_map('strtolower', $item->getActions($this)))) { + return $item->handleAction($this, $actionName, $args, $data); + } + } + throw new InvalidArgumentException("Can't handle action '$actionName'"); + } +} + + +/** + * This class is the base class when you want to have an action that alters the state of the gridfield + * + * @package sapphire + * @subpackage forms + * + */ +class GridField_Action extends FormAction { + + /** + * + * @var GridField + */ + protected $gridField; + + /** + * + * @var string + */ + protected $buttonLabel; + + /** + * + * @var array + */ + protected $stateValues; + + /** + * + * @var array + */ + //protected $stateFields = array(); + + protected $actionName; + protected $args = array(); + + /** + * + * @param GridField $gridField + * @param type $name + * @param type $label + * @param type $actionName + * @param type $args + */ + public function __construct(GridField $gridField, $name, $label, $actionName, $args) { + $this->gridField = $gridField; + $this->buttonLabel = $label; + $this->actionName = $actionName; + $this->args = $args; + parent::__construct($name); + } + + /** + * urlencode encodes less characters in percent form than we need - we need everything that isn't a \w + * + * @param string $val + */ + public function nameEncode($val) { + return preg_replace_callback('/[^\w]/', array($this, '_nameEncode'), $val); + } + + /** + * The callback for nameEncode + * + * @param string $val + */ + public function _nameEncode($match) { + return '%'.dechex(ord($match[0])); + } + + /** + * Default method used by Templates to render the form + * + * @return string HTML tag + */ + public function Field() { + // Store state in session, and pass ID to client side + $state = array( + 'grid' => $this->getNameFromParent(), + 'actionName' => $this->actionName, + 'args' => $this->args, + ); + + $id = preg_replace('/[^\w]+/', '_', uniqid('', true)); + Session::set($id, $state); + + $actionData['StateID'] = $id; + + // And generate field + $attributes = array( + 'class' => 'action' . ($this->extraClass() ? $this->extraClass() : ''), + 'id' => $this->id(), + 'type' => 'submit', + // Note: This field needs to be less than 65 chars, otherwise Suhosin security patch + // will strip it from the requests + 'name' => 'action_gridFieldAlterAction'. '?' . http_build_query($actionData), + 'tabindex' => $this->getTabIndex(), + ); + + if($this->isReadonly()) { + $attributes['disabled'] = 'disabled'; + $attributes['class'] = $attributes['class'] . ' disabled'; + } + + return $this->createTag('button', $attributes, $this->buttonLabel); + } + + /** + * Calculate the name of the gridfield relative to the Form + * + * @param GridField $base + * @return string + */ + protected function getNameFromParent() { + $base = $this->gridField; + $name = array(); + do { + array_unshift($name, $base->getName()); + $base = $base->getForm(); + } while ($base && !($base instanceof Form)); + return implode('.', $name); + } +} diff --git a/forms/gridfield/GridFieldComponent.php b/forms/gridfield/GridFieldComponent.php new file mode 100644 index 000000000..68bad5712 --- /dev/null +++ b/forms/gridfield/GridFieldComponent.php @@ -0,0 +1,44 @@ +getComponents()->push($component); + return $this; + } + + /** + * + * @return ArrayList + */ + public function getComponents() { + if(!$this->components) { + $this->components = new ArrayList(); + } + return $this->components; + } + + public function setCheckboxes($row=0){ + $this->checkboxes = $row; + return $this; + } + + public function getCheckboxes() { + return $this->checkboxes; + } + + public function addAffector(GridState_Affector $affector) { + $this->affectors[] = $affector; + return $this; + } + + public function getAffectors() { + return $this->affectors; + } + + public function addDecorator($decorator) { + $this->decorators[] = $decorator; + } + + public function getDecorators() { + return $this->decorators; + } +} diff --git a/forms/gridfield/GridFieldDefaultColumns.php b/forms/gridfield/GridFieldDefaultColumns.php new file mode 100644 index 000000000..a746e5f57 --- /dev/null +++ b/forms/gridfield/GridFieldDefaultColumns.php @@ -0,0 +1,120 @@ +getDisplayFields()); + foreach($baseColumns as $col) $columns[] = $col; + } + + public function getColumnsHandled($gridField) { + return array_keys($gridField->getDisplayFields()); + } + + /** + * + * @param string $fieldName + * @param string $value + * @param boolean $xmlSafe + * @return type + */ + public function getColumnContent($gridField, $item, $column) { + // Find the data column for the given named column + $fieldName = $column; + $xmlSafe = true; + + // This supports simple FieldName syntax + if(strpos($fieldName, '.') === false) { + return ($item->XML_val($fieldName) && $xmlSafe) ? $item->XML_val($fieldName) : $item->RAW_val($fieldName); + } + $fieldNameParts = explode('.', $fieldName); + $tmpItem = $item; + for($idx = 0; $idx < sizeof($fieldNameParts); $idx++) { + $relationMethod = $fieldNameParts[$idx]; + // Last value for value + if($idx == sizeof($fieldNameParts) - 1) { + if($tmpItem) { + return ($tmpItem->XML_val($relationMethod) && $xmlSafe) ? $tmpItem->XML_val($relationMethod) : $tmpItem->RAW_val($relationMethod); + } + // else get the object for the next iteration + } else { + if($tmpItem) { + $tmpItem = $tmpItem->$relationMethod(); + } + } + } + + $value = $this->castValue($gridField, $column, $value); + $value = $this->formatValue($gridField, $item, $column, $value); + $value = $this->escapeValue($gridField, $value); + + return $value; + } + + public function getColumnAttributes($gridField, $item, $column) { + return null; + } + + public function getColumnMetadata($gridField, $column) { + $columns = $gridField->getDisplayFields(); + return array( + 'title' => $columns[$column], + ); + } + + /** + * + * @param type $fieldName + * @param type $value + * @return type + */ + protected function castValue($gridField, $fieldName, $value) { + if(array_key_exists($fieldName, $gridField->FieldCasting)) { + return $gridField->getCastedValue($value, $gridField->FieldCasting[$fieldName]); + } elseif(is_object($value) && method_exists($value, 'Nice')) { + return $value->Nice(); + } + return $value; + } + + /** + * + * @param type $fieldName + * @param type $value + * @return type + */ + protected function formatValue($gridField, $item, $fieldName, $value) { + if(!array_key_exists($fieldName, $gridField->FieldFormatting)) { + return $value; + } + + $format = str_replace('$value', "__VAL__", $gridField->FieldFormatting[$fieldName]); + $format = preg_replace('/\$([A-Za-z0-9-_]+)/', '$item->$1', $format); + $format = str_replace('__VAL__', '$value', $format); + eval('$value = "' . $format . '";'); + return $value; + } + + /** + * Remove values from a value using FieldEscape setter + * + * @param type $value + * @return type + */ + protected function escapeValue($gridField, $value) { + if(!$escape = $gridField->FieldEscape) { + return $value; + } + + foreach($escape as $search => $replace) { + $value = str_replace($search, $replace, $value); + } + return $value; + } +} \ No newline at end of file diff --git a/forms/gridfield/GridFieldFilter.php b/forms/gridfield/GridFieldFilter.php new file mode 100644 index 000000000..f8d082ed7 --- /dev/null +++ b/forms/gridfield/GridFieldFilter.php @@ -0,0 +1,105 @@ +State->GridFieldFilter; + if($actionName === 'filter') { + if(isset($data['filter'])){ + foreach($data['filter'] as $key => $filter ){ + $state->Columns->$key = $filter; + } + } + } elseif($actionName === 'reset') { + $state->Columns = null; + } + } + + + /** + * + * @param GridField $gridField + * @param SS_List $dataList + * @return SS_List + */ + public function getManipulatedData(GridField $gridField, SS_List $dataList) { + $state = $gridField->State->GridFieldFilter; + if(!isset($state->Columns)) { + return $dataList; + } + + $filterArguments = $state->Columns->toArray(); + foreach($filterArguments as $columnName => $value ) { + if($dataList->canFilterBy($columnName) && $value) { + $dataList->filter($columnName.':PartialMatch', $value); + } + } + return $dataList; + } + + public function getHTMLFragments($gridField) { + Requirements::javascript(SAPPHIRE_DIR.'/thirdparty/jquery-entwine/dist/jquery.entwine-dist.js'); + Requirements::javascript('sapphire/javascript/GridField.js'); + + $forTemplate = new ArrayData(array()); + $forTemplate->Fields = new ArrayList; + + $columns = $gridField->getColumns(); + $filterArguments = $gridField->State->GridFieldFilter->Columns->toArray(); + + $currentColumn = 0; + foreach($columns as $columnField) { + $currentColumn++; + $metadata = $gridField->getColumnMetadata($columnField); + $title = $metadata['title']; + if($title && $gridField->getList()->canFilterBy($columnField)) { + $value = ''; + if(isset($filterArguments[$columnField])) { + $value = $filterArguments[$columnField]; + } + $field = new TextField('filter['.$columnField.']', 'filter['.$columnField.']', $value); + $field->addExtraClass('ss-gridfield-sort'); + } else { + $field = new LiteralField('', ''); + } + + // Last column, inject action buttons + if($currentColumn == count($columns)) { + $field = new FieldGroup( + $field, + new GridField_Action($gridField, 'filter', 'filter', 'filter', null), + new GridField_Action($gridField, 'reset', 'reset', 'reset', null) + ); + + } + $field->iteratorProperties($currentColumn-1, count($columns)); + $forTemplate->Fields->push($field); + } + + return array( + 'header' => $forTemplate->renderWith('GridFieldFilter_Row'), + ); + } +} \ No newline at end of file diff --git a/forms/gridfield/GridFieldPaginator.php b/forms/gridfield/GridFieldPaginator.php new file mode 100644 index 000000000..8bb5f7c50 --- /dev/null +++ b/forms/gridfield/GridFieldPaginator.php @@ -0,0 +1,90 @@ +itemsPerPage = $itemsPerPage; + } + + public function getActions($gridField) { + return array('paginate'); + } + + public function handleAction(GridField $gridField, $actionName, $arguments, $data) { + if($actionName !== 'paginate') { + return; + } + $state = $gridField->State->GridFieldPaginator; + $this->currentPage = $state->currentPage = (int)$arguments; + } + + /** Duck check to see if list support methods we need to paginate */ + protected function getListPaginatable(SS_List $list) { + // If no list yet, not paginatable + if (!$list) return false; + // Check for methods we use + if(!method_exists($list, 'getRange')) return false; + if(!method_exists($list, 'limit')) return false; + // Default it true + return true; + } + + public function getManipulatedData(GridField $gridField, SS_List $dataList) { + if(!$this->getListPaginatable($dataList)) { + return $dataList; + } + if(!$this->currentPage) { + return $dataList->getRange(0, (int)$this->itemsPerPage); + } + $startRow = $this->itemsPerPage*($this->currentPage-1); + return $dataList->getRange((int)$startRow, (int)$this->itemsPerPage); + } + + public function getHTMLFragments($gridField) { + Requirements::javascript(SAPPHIRE_DIR.'/thirdparty/jquery-entwine/dist/jquery.entwine-dist.js'); + Requirements::javascript(SAPPHIRE_DIR.'/javascript/GridField.js'); + + $forTemplate = new ArrayData(array()); + $forTemplate->Fields = new ArrayList; + + $countList = clone $gridField->List; + $totalRows = $countList->limit(null)->count(); + $totalPages = ceil($totalRows/$this->itemsPerPage); + for($idx=1; $idx<=$totalPages; $idx++) { + if($idx == $this->currentPage) { + $field = new LiteralField('pagination_'.$idx, $idx); + } else { + $field = new GridField_Action($gridField, 'pagination_'.$idx, $idx, 'paginate', $idx); + $field->addExtraClass('ss-gridfield-button'); + } + + $forTemplate->Fields->push($field); + } + if(!$forTemplate->Fields->Count()) { + return array(); + } + return array( + 'footer' => $forTemplate->renderWith('GridFieldPaginator_Row', array('Colspan'=>count($gridField->getColumns()))), + ); + } +} diff --git a/forms/gridfield/GridFieldSortableHeader.php b/forms/gridfield/GridFieldSortableHeader.php new file mode 100644 index 000000000..c9b1572e8 --- /dev/null +++ b/forms/gridfield/GridFieldSortableHeader.php @@ -0,0 +1,82 @@ +Fields = new ArrayList; + + $state = $gridField->State->GridFieldSortableHeader; + $columns = $gridField->getColumns(); + + foreach($columns as $columnField) { + $metadata = $gridField->getColumnMetadata($columnField); + $title = $metadata['title']; + if($title && $gridField->getList()->canSortBy($columnField)) { + $dir = 'asc'; + if($state->SortColumn == $columnField && $state->SortDirection == 'asc') { + $dir = 'desc'; + } + + $field = new GridField_Action($gridField, 'SetOrder'.$columnField, $title, "sort$dir", array('SortColumn' => $columnField)); + + $field->addExtraClass('ss-gridfield-sort'); + if($state->SortColumn == $columnField){ + $field->addExtraClass('ss-gridfield-sorted'); + } + } else { + $field = new LiteralField($columnField, $title); + } + $forTemplate->Fields->push($field); + } + + return array( + 'header' => $forTemplate->renderWith('GridFieldSortableHeader_Row'), + ); + } + + /** + * + * @param GridField $gridField + * @return array + */ + public function getActions($gridField) { + return array('sortasc', 'sortdesc'); + } + + function handleAction(GridField $gridField, $actionName, $arguments, $data) { + $state = $gridField->State->GridFieldSortableHeader; + switch($actionName) { + case 'sortasc': + $state->SortColumn = $arguments['SortColumn']; + $state->SortDirection = 'asc'; + break; + + case 'sortdesc': + $state->SortColumn = $arguments['SortColumn']; + $state->SortDirection = 'desc'; + break; + } + } + + public function getManipulatedData(GridField $gridField, SS_List $dataList) { + $state = $gridField->State->GridFieldSortableHeader; + if ($state->SortColumn == "") { + return $dataList; + } + return $dataList->sort($state->SortColumn, $state->SortDirection); + } +} \ No newline at end of file diff --git a/forms/gridfield/GridState.php b/forms/gridfield/GridState.php new file mode 100644 index 000000000..fb5da4f4d --- /dev/null +++ b/forms/gridfield/GridState.php @@ -0,0 +1,148 @@ +grid = $grid; + + if ($value) $this->setValue($value); + + parent::__construct('GridState'); + } + + /** + * + * @param type $value + */ + public function setValue($value) { + if (is_string($value)) { + $this->gridStateData = new GridState_Data(json_decode($value, true)); + } + parent::setValue($value); + } + + public function getData() { + if(!$this->gridStateData) $this->gridStateData = new GridState_Data; + return $this->gridStateData; + } + + /** + * + * @return type + */ + public function getList() { + return $this->grid->getList(); + } + + /** @return string */ + public function Value() { + return json_encode($this->gridStateData->toArray()); + } + + /** + * + * @return type + */ + public function dataValue() { + return $this->Value(); + } + + /** + * + * @return type + */ + public function attrValue() { + return Convert::raw2att($this->Value()); + } + + /** + * + * @return type + */ + public function __toString() { + return $this->Value(); + } +} + +/** + * Simple set of data, similar to stdClass, but without the notice-level errors + */ +class GridState_Data { + protected $data; + + function __construct($data = array()) { + $this->data = $data; + } + + function __get($name) { + if(!isset($this->data[$name])) $this->data[$name] = new GridState_Data; + if(is_array($this->data[$name])) $this->data[$name] = new GridState_Data($this->data[$name]); + return $this->data[$name]; + } + function __set($name, $value) { + $this->data[$name] = $value; + } + function __isset($name) { + return isset($this->data[$name]); + } + + function __toString() { + if(!$this->data) return ""; + else return json_encode($this->toArray()); + } + + function toArray() { + $output = array(); + foreach($this->data as $k => $v) { + $output[$k] = (is_object($v) && method_exists($v, 'toArray')) ? $v->toArray() : $v; + } + return $output; + } +} + + +class GridState_Component implements GridField_HTMLProvider { + + public function getHTMLFragments($gridField) { + + $forTemplate = new ArrayData(array()); + $forTemplate->Fields = new ArrayList; + + return array( + 'before' => $gridField->getState(false)->Field() + ); + } + +} \ No newline at end of file diff --git a/javascript/GridField.js b/javascript/GridField.js new file mode 100644 index 000000000..546a62b4c --- /dev/null +++ b/javascript/GridField.js @@ -0,0 +1,70 @@ +jQuery(function($){ + + $('.ss-gridfield .action').entwine({ + onclick: function(e){ + button = this; + e.preventDefault(); + var form = $(this).closest("form"); + form.addClass('loading'); + $.ajax({ + type: "POST", + url: form.attr('action'), + data: form.serialize()+'&'+escape(button.attr('name'))+'='+escape(button.val()), + dataType: 'html', + success: function(data) { + form.replaceWith(data); + form.removeClass('loading'); + }, + error: function(e) { + alert(ss.i18n._t('GRIDFIELD.ERRORINTRANSACTION', 'An error occured while fetching data from the server\n Please try again later.')); + form.removeClass('loading'); + } + }); + } + }); + + var removeFilterButtons = function() { + // Remove stuff + $('th').children('div').each(function(i,v) { + $(v).remove(); + }); + } + + /* + * Upon focusing on a filter element, move "filter" and "reset" buttons and display next to the current element + * ToDo ensure filter-button state is maintained after filtering (see resetState param) + * ToDo get working in IE 6-7 + */ + $('.ss-gridfield input.ss-gridfield-sort').entwine({ + onfocusin: function(e) { + // Dodgy results in IE <=7 + if($.browser.msie && $.browser.version <= 7) { + return false; + } + var eleInput = $(this); + // Remove existing
and - <% end_if %> - - <% if PreviousLink %> - - <% end_if %> - - <% control Pages %> - <% if Current %> - $PageNumber - <% else %> - - <% end_if %> - <% end_control%> - - <% if NextLink %> - - <% end_if %> - - <% if LastLink %> - - <% end_if %> -
-<% end_if %> \ No newline at end of file diff --git a/templates/GridFieldPresenter.ss b/templates/GridFieldPresenter.ss deleted file mode 100644 index 86c633be5..000000000 --- a/templates/GridFieldPresenter.ss +++ /dev/null @@ -1,29 +0,0 @@ -<% require css(sapphire/thirdparty/jquery-ui-themes/smoothness/jquery-ui.css) %> -<% require css(sapphire/css/GridField.css) %> - -
- - - - <% control Headers %> - - <% end_control %> - - - - - <% control Items %> - <% include GridField_Item %> - <% end_control %> - - - - -
- $Title
- - <% control Footers %> - $Render - <% end_control %> - -
diff --git a/templates/Includes/GridFieldFilter_Row.ss b/templates/Includes/GridFieldFilter_Row.ss new file mode 100644 index 000000000..7bb481e6a --- /dev/null +++ b/templates/Includes/GridFieldFilter_Row.ss @@ -0,0 +1,5 @@ + + <% control Fields %> + $Field + <% end_control %> + \ No newline at end of file diff --git a/templates/Includes/GridFieldPaginator_Row.ss b/templates/Includes/GridFieldPaginator_Row.ss new file mode 100644 index 000000000..eea032019 --- /dev/null +++ b/templates/Includes/GridFieldPaginator_Row.ss @@ -0,0 +1,7 @@ + + + <% control Fields %> + $Field + <% end_control %> + + \ No newline at end of file diff --git a/templates/Includes/GridFieldSortableHeader_Row.ss b/templates/Includes/GridFieldSortableHeader_Row.ss new file mode 100644 index 000000000..a1241d2cb --- /dev/null +++ b/templates/Includes/GridFieldSortableHeader_Row.ss @@ -0,0 +1,5 @@ + + <% control Fields %> + $Field + <% end_control %> + diff --git a/templates/Includes/GridField_Item.ss b/templates/Includes/GridField_Item.ss index d49120690..2b28e2f31 100644 --- a/templates/Includes/GridField_Item.ss +++ b/templates/Includes/GridField_Item.ss @@ -1,5 +1,12 @@ - <% control Fields %> - class="ss-gridfield-{$FirstLast}"<% end_if %>>$Value - <% end_control %> + <% if $GridField.ExtraColumnsCount %> + <% control Fields %> + $Value + <% end_control %> + + <% else %> + <% control Fields %> + class="ss-gridfield-{$FirstLast}"<% end_if %>>$Value + <% end_control %> + <% end_if %> \ No newline at end of file diff --git a/tests/control/RequestHandlingTest.php b/tests/control/RequestHandlingTest.php index 1846456a7..493a9a6c0 100644 --- a/tests/control/RequestHandlingTest.php +++ b/tests/control/RequestHandlingTest.php @@ -234,6 +234,17 @@ class RequestHandlingTest extends FunctionalTest { $this->assertContains('not allowed on form', $response->getBody()); } + function testActionHandlingOnField() { + $data = array('action_actionOnField' => 1); + $response = $this->post('RequestHandlingFieldTest_Controller/TestForm', $data); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals('Test method on MyField', $response->getBody()); + + $data = array('action_actionNotAllowedOnField' => 1); + $response = $this->post('RequestHandlingFieldTest_Controller/TestForm', $data); + $this->assertEquals(404, $response->getStatusCode()); + } + } /** @@ -544,3 +555,32 @@ class RequestHandlingTest_SubclassedFormField extends RequestHandlingTest_FormFi return "customSomething"; } } + + +/** + * Controller for the test + */ +class RequestHandlingFieldTest_Controller extends Controller implements TestOnly { + + function TestForm() { + return new Form($this, "TestForm", new FieldList( + new RequestHandlingTest_HandlingField("MyField") + ), new FieldList( + new FormAction("myAction") + )); + } +} + +/** + * Form field for the test + */ +class RequestHandlingTest_HandlingField extends FormField { + + static $allowed_actions = array( + 'actionOnField' + ); + + function actionOnField() { + return "Test method on $this->name"; + } +} diff --git a/tests/filesystem/FileTest.php b/tests/filesystem/FileTest.php index b09483951..e89baa1d7 100644 --- a/tests/filesystem/FileTest.php +++ b/tests/filesystem/FileTest.php @@ -13,7 +13,7 @@ class FileTest extends SapphireTest { // Note: We can't use fixtures/setUp() for this, as we want to create the db record manually. // Creating the folder is necessary to avoid having "Filename" overwritten by setName()/setRelativePath(), // because the parent folders don't exist in the database - $folder = Folder::findOrMake('/FileTest/'); + $folder = Folder::find_or_make('/FileTest/'); $testfilePath = 'assets/FileTest/CreateWithFilenameHasCorrectPath.txt'; // Important: No leading slash $fh = fopen(BASE_PATH . '/' . $testfilePath, "w"); fwrite($fh, str_repeat('x',1000000)); diff --git a/tests/filesystem/FolderTest.php b/tests/filesystem/FolderTest.php index d6dd6dbe9..c1fd27f5e 100644 --- a/tests/filesystem/FolderTest.php +++ b/tests/filesystem/FolderTest.php @@ -33,7 +33,7 @@ class FolderTest extends SapphireTest { function testFindOrMake() { $path = '/FolderTest/testFindOrMake/'; - $folder = Folder::findOrMake($path); + $folder = Folder::find_or_make($path); $this->assertEquals(ASSETS_DIR . $path,$folder->getRelativePath(), 'Nested path information is correctly saved to database (with trailing slash)' ); @@ -44,13 +44,13 @@ class FolderTest extends SapphireTest { $this->assertEquals($parentFolder->ID, $folder->ParentID); $path = '/FolderTest/testFindOrMake'; // no trailing slash - $folder = Folder::findOrMake($path); + $folder = Folder::find_or_make($path); $this->assertEquals(ASSETS_DIR . $path . '/',$folder->getRelativePath(), 'Path information is correctly saved to database (without trailing slash)' ); $path = '/assets/'; // relative to "assets/" folder, should produce "assets/assets/" - $folder = Folder::findOrMake($path); + $folder = Folder::find_or_make($path); $this->assertEquals(ASSETS_DIR . $path,$folder->getRelativePath(), 'A folder named "assets/" within "assets/" is allowed' ); @@ -126,7 +126,7 @@ class FolderTest extends SapphireTest { */ function testFindOrMakeFolderThenMove() { $folder1 = $this->objFromFixture('Folder', 'folder1'); - Folder::findOrMake($folder1->Filename); + Folder::find_or_make($folder1->Filename); $folder2 = $this->objFromFixture('Folder', 'folder2'); // set ParentID @@ -182,7 +182,7 @@ class FolderTest extends SapphireTest { function testDeleteAlsoRemovesFilesystem() { $path = '/FolderTest/DeleteAlsoRemovesFilesystemAndChildren'; - $folder = Folder::findOrMake($path); + $folder = Folder::find_or_make($path); $this->assertFileExists(ASSETS_PATH . $path); $folder->delete(); @@ -193,8 +193,8 @@ class FolderTest extends SapphireTest { function testDeleteAlsoRemovesSubfoldersInDatabaseAndFilesystem() { $path = '/FolderTest/DeleteAlsoRemovesSubfoldersInDatabaseAndFilesystem'; $subfolderPath = $path . '/subfolder'; - $folder = Folder::findOrMake($path); - $subfolder = Folder::findOrMake($subfolderPath); + $folder = Folder::find_or_make($path); + $subfolder = Folder::find_or_make($subfolderPath); $subfolderID = $subfolder->ID; $folder->delete(); @@ -206,7 +206,7 @@ class FolderTest extends SapphireTest { function testDeleteAlsoRemovesContainedFilesInDatabaseAndFilesystem() { $path = '/FolderTest/DeleteAlsoRemovesContainedFilesInDatabaseAndFilesystem'; - $folder = Folder::findOrMake($path); + $folder = Folder::find_or_make($path); $file = $this->objFromFixture('File', 'gif'); $file->ParentID = $folder->ID; diff --git a/tests/forms/GridFieldFunctionalTest.php b/tests/forms/GridFieldFunctionalTest.php deleted file mode 100644 index 67d2fc0df..000000000 --- a/tests/forms/GridFieldFunctionalTest.php +++ /dev/null @@ -1,45 +0,0 @@ -objFromFixture('GridFieldTest_Person', 'first'); - $response = $this->get("GridFieldFunctionalTest_Controller/"); - $this->assertContains($firstPerson->Name, $response->getBody()); - } -} - -class GridFieldFunctionalTest_Controller extends Controller { - - protected $template = 'BlankPage'; - - function Link($action = null) { - return Controller::join_links('GridFieldFunctionalTest_Controller', $action); - } - - public function index() { - $grid = new GridField('testgrid'); - $dataSource = DataList::create("GridFieldTest_Person")->sort("Name"); - $grid->setList($dataSource); - $form = new Form($this, 'gridform', new FieldList($grid), new FieldList(new FormAction('rerender', 'rerender'))); - return array('Form'=>$form); - } -} \ No newline at end of file diff --git a/tests/forms/GridFieldPaginatorTest.php b/tests/forms/GridFieldPaginatorTest.php deleted file mode 100644 index 644095f7d..000000000 --- a/tests/forms/GridFieldPaginatorTest.php +++ /dev/null @@ -1,38 +0,0 @@ -assertTrue(new GridFieldPaginator(1,1) instanceof GridFieldPaginator, 'Trying to find an instance of GridFieldPaginator'); - $this->assertTrue(new GridFieldPaginator_Extension() instanceof GridFieldPaginator_Extension, 'Trying to find an instance of GridFieldPaginator_Extension'); - } - - public function testFlowThroughGridFieldExtension() { - $list = new DataList('GridFieldTest_Person'); - $t = new GridFieldPaginator_Extension(); - $t->paginationLimit(5); - - $parameters = new stdClass(); - $parameters->Request = new SS_HTTPRequest('GET', '/a/url', array('page'=>1)); - - $t->filterList($list, $parameters); - $this->assertTrue($t->Footer() instanceof GridFieldPaginator); - } -} \ No newline at end of file diff --git a/tests/forms/GridFieldPresenterTest.php b/tests/forms/GridFieldPresenterTest.php deleted file mode 100755 index d5ff36ab4..000000000 --- a/tests/forms/GridFieldPresenterTest.php +++ /dev/null @@ -1,61 +0,0 @@ -assertTrue(new GridFieldPresenter instanceof GridFieldPresenter, 'Trying to find an instance of GridFieldPresenter'); - } - - public function testHeaders() { - $presenter = new GridFieldPresenter(); - $grid = new GridField('testgrid', 'testgrid', new DataList('GridFieldTest_Person')); - $presenter->setGridField($grid); - $headers = $presenter->Headers()->first(); - - $this->assertEquals(1, count($headers)); - $this->assertEquals('Name', $headers->Name ); - } - - public function testItemsReturnCorrectNumberOfItems() { - $presenter = new GridFieldPresenter(); - $grid = new GridField('testgrid', 'testgrid', new DataList('GridFieldTest_Person')); - $presenter->setGridField($grid); - $this->assertEquals(2, $presenter->Items()->count()); - } - - public function testSorting(){ - $presenter = new GridFieldPresenter(); - $GridField = new GridField('testgrid', 'testgrid', new DataList('GridFieldTest_Person')); - $presenter->setGridField($GridField); - $presenter->sort('Name','desc'); - $data = $presenter->Items()->map('ID','Name'); - $this->assertEquals(array( - $this->idFromFixture('GridFieldTest_Person', 'second') => 'Second Person', - $this->idFromFixture('GridFieldTest_Person', 'first') => 'First Person' - ), $data); - $presenter->sort('Name','asc'); - $data = $presenter->Items()->map('ID','Name'); - $this->assertEquals(array( - $this->idFromFixture('GridFieldTest_Person', 'first') => 'First Person', - $this->idFromFixture('GridFieldTest_Person', 'second') => 'Second Person' - ), $data); - } -} \ No newline at end of file diff --git a/tests/forms/GridFieldTest.php b/tests/forms/GridFieldTest.php deleted file mode 100644 index 424ac929a..000000000 --- a/tests/forms/GridFieldTest.php +++ /dev/null @@ -1,94 +0,0 @@ -assertTrue(new GridField('Testgrid') instanceof FormField, 'GridField should be a FormField'); - } - - public function testSetDataSource() { - $grid = new GridField('Testgrid'); - $source = new ArrayList(); - $grid->setList($source); - $this->assertEquals($source, $grid->getList()); - } - - function testSetEmptyDataPresenter() { - $this->setExpectedException('InvalidArgumentException'); - $grid = new GridField('Testgrid'); - $grid->setPresenter(''); - } - - function testSetNonExistingDataPresenter() { - $this->setExpectedException('InvalidArgumentException'); - $grid = new GridField('Testgrid'); - $grid->setPresenter('ifThisClassExistsIWouldBeSurprised'); - } - - function testSetDataPresenterWithDataObject() { - $this->setExpectedException('InvalidArgumentException'); - $grid = new GridField('Testgrid'); - $grid->setPresenter('DataObject'); - } - - function testSetDataPresenter() { - $grid = new GridField('Testgrid'); - $grid->setPresenter('GridFieldPresenter'); - } - - function testSetDataclass() { - $grid = new GridField('Testgrid'); - $grid->setModelClass('SiteTree'); - $this->assertEquals('SiteTree', $grid->getModelClass()); - } - - /** - * - */ - function testFieldHolderWithoutDataSource() { - $this->setExpectedException('LogicException'); - $grid = new GridField('Testgrid'); - $this->assertNotNull($grid->FieldHolder()); - } - - /** - * This is better tested in the GridFieldFunctionalTest - * - * @see GridFieldFunctionalTest - */ - function testFieldHolder() { - $grid = new GridField('Testgrid'); - $grid->setList(new DataList('GridFieldTest_Person')); - $this->assertNotNull($grid->FieldHolder()); - } -} - -class GridFieldTest_Person extends Dataobject implements TestOnly { - - public static $db = array( - 'Name' => 'Varchar' - ); - - public static $summary_fields = array( - 'Name', - 'ID' - ); -} \ No newline at end of file diff --git a/tests/forms/GridFieldTest.yml b/tests/forms/GridFieldTest.yml deleted file mode 100644 index a1d123193..000000000 --- a/tests/forms/GridFieldTest.yml +++ /dev/null @@ -1,5 +0,0 @@ -GridFieldTest_Person: - first: - Name: First Person - second: - Name: Second Person \ No newline at end of file