1474 lines
40 KiB
PHP
Raw Normal View History

2014-05-14 23:24:20 +10:00
<?php namespace Backend\Widgets;
use Db;
2016-01-02 14:59:59 +11:00
use Html;
2014-05-14 23:24:20 +10:00
use App;
use Lang;
use Input;
use Event;
use Backend;
2014-05-20 15:45:20 +10:00
use DbDongle;
use Carbon\Carbon;
use October\Rain\Html\Helper as HtmlHelper;
2014-05-14 23:24:20 +10:00
use October\Rain\Router\Helper as RouterHelper;
use System\Helpers\DateTime as DateTimeHelper;
use System\Classes\PluginManager;
2014-05-14 23:24:20 +10:00
use Backend\Classes\ListColumn;
use Backend\Classes\WidgetBase;
2015-01-28 18:03:35 +11:00
use ApplicationException;
2014-05-14 23:24:20 +10:00
use October\Rain\Database\Model;
use DateTime;
2014-05-14 23:24:20 +10:00
/**
* List Widget
* Used for building back end lists, renders a list of model objects
*
* @package october\backend
* @author Alexey Bobkov, Samuel Georges
*/
class Lists extends WidgetBase
{
//
// Configurable properties
//
2014-05-14 23:24:20 +10:00
/**
* @var array List column configuration.
2014-05-14 23:24:20 +10:00
*/
public $columns;
2014-05-14 23:24:20 +10:00
/**
* @var Model List model object.
*/
public $model;
/**
* @var string Link for each record row. Replace :id with the record id.
2014-05-14 23:24:20 +10:00
*/
public $recordUrl;
2014-05-14 23:24:20 +10:00
/**
* @var string Click event for each record row. Replace :id with the record id.
2014-05-14 23:24:20 +10:00
*/
public $recordOnClick;
2014-05-14 23:24:20 +10:00
/**
* @var string Message to display when there are no records in the list.
2014-05-14 23:24:20 +10:00
*/
public $noRecordsMessage = 'backend::lang.list.no_records';
2014-05-14 23:24:20 +10:00
/**
* @var int Maximum rows to display for each page.
2014-05-14 23:24:20 +10:00
*/
public $recordsPerPage;
2014-05-14 23:24:20 +10:00
/**
* @var bool Shows the sorting options for each column.
2014-05-14 23:24:20 +10:00
*/
public $showSorting = true;
2014-05-14 23:24:20 +10:00
/**
* @var mixed A default sort column to look for.
2014-05-14 23:24:20 +10:00
*/
public $defaultSort;
2014-05-14 23:24:20 +10:00
/**
* @var bool Display a checkbox next to each record row.
2014-05-14 23:24:20 +10:00
*/
public $showCheckboxes = false;
2014-05-14 23:24:20 +10:00
/**
* @var bool Display the list set up used for column visibility and ordering.
*/
public $showSetup = false;
2014-05-14 23:24:20 +10:00
/**
* @var bool Display parent/child relationships in the list.
2014-05-14 23:24:20 +10:00
*/
public $showTree = false;
2014-05-14 23:24:20 +10:00
/**
* @var bool Expand the tree nodes by default.
2014-05-14 23:24:20 +10:00
*/
public $treeExpanded = false;
2014-08-13 21:23:19 +10:00
/**
* @var bool|string Display pagination when limiting records per page.
2014-08-13 21:23:19 +10:00
*/
public $showPagination = 'auto';
/**
* @var string Specify a custom view path to override partials used by the list.
*/
public $customViewPath;
//
// Object properties
//
2014-05-14 23:24:20 +10:00
/**
* {@inheritDoc}
2014-05-14 23:24:20 +10:00
*/
protected $defaultAlias = 'list';
2014-05-14 23:24:20 +10:00
/**
* @var array Collection of all list columns used in this list.
* @see Backend\Classes\ListColumn
2014-05-14 23:24:20 +10:00
*/
protected $allColumns;
2014-05-14 23:24:20 +10:00
/**
* @var array Override default columns with supplied key names.
2014-05-14 23:24:20 +10:00
*/
protected $columnOverride;
2014-05-14 23:24:20 +10:00
/**
* @var array Columns to display and their order.
2014-05-14 23:24:20 +10:00
*/
protected $visibleColumns;
2014-05-14 23:24:20 +10:00
/**
* @var array Model data collection.
2014-05-14 23:24:20 +10:00
*/
protected $records;
2014-05-14 23:24:20 +10:00
/**
* @var int Current page number.
2014-05-14 23:24:20 +10:00
*/
protected $currentPageNumber;
2014-05-14 23:24:20 +10:00
/**
* @var string Filter the records by a search term.
2014-05-14 23:24:20 +10:00
*/
protected $searchTerm;
2014-05-14 23:24:20 +10:00
/**
* @var string If searching the records, specifies a policy to use.
* - all: result must contain all words
* - any: result can contain any word
* - exact: result must contain the exact phrase
*/
protected $searchMode;
/**
* @var string Use a custom scope method for performing searches.
*/
protected $searchScope;
2014-05-14 23:24:20 +10:00
/**
* @var array Collection of functions to apply to each list query.
2014-05-14 23:24:20 +10:00
*/
protected $filterCallbacks = [];
2014-05-14 23:24:20 +10:00
/**
* @var array All sortable columns.
2014-05-14 23:24:20 +10:00
*/
protected $sortableColumns;
2014-05-14 23:24:20 +10:00
/**
* @var string Sets the list sorting column.
2014-05-14 23:24:20 +10:00
*/
protected $sortColumn;
/**
* @var string Sets the list sorting direction (asc, desc)
*/
protected $sortDirection;
2014-05-14 23:24:20 +10:00
/**
* @var array List of CSS classes to apply to the list container element
*/
public $cssClasses = [];
2014-05-14 23:24:20 +10:00
/**
* Initialize the widget, called by the constructor and free from its parameters.
*/
public function init()
{
$this->fillFromConfig([
'columns',
'model',
'recordUrl',
'recordOnClick',
'noRecordsMessage',
'recordsPerPage',
'showSorting',
'defaultSort',
'showCheckboxes',
'showSetup',
'showTree',
'treeExpanded',
'showPagination',
'customViewPath',
]);
2014-05-14 23:24:20 +10:00
/*
* Configure the list widget
*/
$this->recordsPerPage = $this->getSession('per_page', $this->recordsPerPage);
if ($this->showPagination == 'auto') {
$this->showPagination = $this->recordsPerPage && $this->recordsPerPage > 0;
}
if ($this->customViewPath) {
$this->addViewPath($this->customViewPath);
}
$this->validateModel();
2014-05-14 23:24:20 +10:00
$this->validateTree();
}
/**
* {@inheritDoc}
*/
protected function loadAssets()
2014-05-14 23:24:20 +10:00
{
2014-05-24 16:57:38 +10:00
$this->addJs('js/october.list.js', 'core');
2014-05-14 23:24:20 +10:00
}
/**
* Renders the widget.
*/
public function render()
{
$this->prepareVars();
2014-06-30 18:27:17 +10:00
return $this->makePartial('list-container');
2014-05-14 23:24:20 +10:00
}
/**
* Prepares the list data
*/
public function prepareVars()
{
$this->vars['cssClasses'] = implode(' ', $this->cssClasses);
$this->vars['columns'] = $this->getVisibleColumns();
2014-05-14 23:24:20 +10:00
$this->vars['columnTotal'] = $this->getTotalColumns();
$this->vars['records'] = $this->getRecords();
$this->vars['noRecordsMessage'] = trans($this->noRecordsMessage);
$this->vars['showCheckboxes'] = $this->showCheckboxes;
$this->vars['showSetup'] = $this->showSetup;
$this->vars['showPagination'] = $this->showPagination;
$this->vars['showSorting'] = $this->showSorting;
$this->vars['sortColumn'] = $this->getSortColumn();
$this->vars['sortDirection'] = $this->sortDirection;
$this->vars['showTree'] = $this->showTree;
$this->vars['treeLevel'] = 0;
if ($this->showPagination) {
$this->vars['recordTotal'] = $this->records->total();
$this->vars['pageCurrent'] = $this->records->currentPage();
$this->vars['pageLast'] = $this->records->lastPage();
$this->vars['pageFrom'] = $this->records->firstItem();
$this->vars['pageTo'] = $this->records->lastItem();
}
else {
2014-05-14 23:24:20 +10:00
$this->vars['recordTotal'] = $this->records->count();
$this->vars['pageCurrent'] = 1;
}
}
/**
* Event handler for refreshing the list.
*/
public function onRefresh()
2014-05-14 23:24:20 +10:00
{
$this->prepareVars();
return ['#'.$this->getId() => $this->makePartial('list')];
}
/**
* Event handler for switching the page number.
*/
public function onPaginate()
{
$this->currentPageNumber = post('page');
return $this->onRefresh();
2014-05-14 23:24:20 +10:00
}
/**
* Validate the supplied form model.
* @return void
*/
protected function validateModel()
{
2014-10-11 00:39:34 +02:00
if (!$this->model) {
throw new ApplicationException(Lang::get(
'backend::lang.list.missing_model',
['class'=>get_class($this->controller)]
));
}
2014-05-14 23:24:20 +10:00
2014-10-11 00:39:34 +02:00
if (!$this->model instanceof Model) {
throw new ApplicationException(Lang::get(
'backend::lang.model.invalid_class',
['model'=>get_class($this->model), 'class'=>get_class($this->controller)]
));
}
2014-05-14 23:24:20 +10:00
return $this->model;
}
/**
* Replaces the @ symbol with a table name in a model
* @param string $sql
* @param string $table
* @return string
*/
2014-08-01 18:18:09 +10:00
protected function parseTableName($sql, $table)
2014-05-14 23:24:20 +10:00
{
return str_replace('@', $table.'.', $sql);
}
/**
* Applies any filters to the model.
*/
public function prepareModel()
2014-05-14 23:24:20 +10:00
{
$query = $this->model->newQuery();
$primaryTable = $this->model->getTable();
$selects = [$primaryTable.'.*'];
2014-05-14 23:24:20 +10:00
$joins = [];
$withs = [];
2014-05-14 23:24:20 +10:00
/*
* Extensibility
*/
Event::fire('backend.list.extendQueryBefore', [$this, $query]);
$this->fireEvent('list.extendQueryBefore', [$query]);
2014-05-14 23:24:20 +10:00
/*
* Prepare searchable column names
2014-05-14 23:24:20 +10:00
*/
$primarySearchable = [];
$relationSearchable = [];
2014-05-14 23:24:20 +10:00
$columnsToSearch = [];
if (!empty($this->searchTerm) && ($searchableColumns = $this->getSearchableColumns())) {
foreach ($searchableColumns as $column) {
/*
* Related
*/
if ($this->isColumnRelated($column)) {
$table = $this->model->makeRelation($column->relation)->getTable();
$columnName = isset($column->sqlSelect)
? DbDongle::raw($this->parseTableName($column->sqlSelect, $table))
2014-09-17 19:08:49 +10:00
: $table . '.' . $column->valueFrom;
$relationSearchable[$column->relation][] = $columnName;
}
/*
* Primary
*/
else {
$columnName = isset($column->sqlSelect)
? DbDongle::raw($this->parseTableName($column->sqlSelect, $primaryTable))
: DbDongle::cast(Db::getTablePrefix() . $primaryTable . '.' . $column->columnName, 'TEXT');
$primarySearchable[] = $columnName;
}
}
}
2014-05-14 23:24:20 +10:00
/*
* Prepare related eager loads (withs) and custom selects (joins)
*/
foreach ($this->getVisibleColumns() as $column) {
2014-05-16 11:49:08 +10:00
2014-10-11 00:39:34 +02:00
if (!$this->isColumnRelated($column) || (!isset($column->sqlSelect) && !isset($column->valueFrom))) {
continue;
2014-10-11 00:39:34 +02:00
}
2014-10-11 00:39:34 +02:00
if (isset($column->valueFrom)) {
$withs[] = $column->relation;
2014-10-11 00:39:34 +02:00
}
2014-05-14 23:24:20 +10:00
$joins[] = $column->relation;
}
/*
* Add eager loads to the query
*/
if ($withs) {
$query->with(array_unique($withs));
}
2014-05-14 23:24:20 +10:00
/*
* Apply search term
*/
$query->where(function ($innerQuery) use ($primarySearchable, $relationSearchable, $joins) {
/*
* Search primary columns
*/
if (count($primarySearchable) > 0) {
$this->applySearchToQuery($innerQuery, $primarySearchable, 'or');
}
/*
* Search relation columns
*/
if ($joins) {
foreach (array_unique($joins) as $join) {
/*
* Apply a supplied search term for relation columns and
* constrain the query only if there is something to search for
*/
$columnsToSearch = array_get($relationSearchable, $join, []);
if (count($columnsToSearch) > 0) {
$innerQuery->orWhereHas($join, function ($_query) use ($columnsToSearch) {
$this->applySearchToQuery($_query, $columnsToSearch);
});
}
}
}
});
2014-05-14 23:24:20 +10:00
/*
* Custom select queries
*/
foreach ($this->getVisibleColumns() as $column) {
2014-10-11 00:39:34 +02:00
if (!isset($column->sqlSelect)) {
2014-05-14 23:24:20 +10:00
continue;
2014-10-11 00:39:34 +02:00
}
2014-05-14 23:24:20 +10:00
$alias = $query->getQuery()->getGrammar()->wrap($column->columnName);
/*
* Relation column
*/
if (isset($column->relation)) {
// @todo Find a way...
$relationType = $this->model->getRelationType($column->relation);
if ($relationType == 'morphTo') {
throw new ApplicationException('The relationship morphTo is not supported for list columns.');
}
$table = $this->model->makeRelation($column->relation)->getTable();
$sqlSelect = $this->parseTableName($column->sqlSelect, $table);
/*
* Manipulate a count query for the sub query
*/
$relationObj = $this->model->{$column->relation}();
$countQuery = $relationObj->getRelationCountQuery($relationObj->getRelated()->newQueryWithoutScopes(), $query);
$joinSql = $this->isColumnRelated($column, true)
? DbDongle::raw("group_concat(" . $sqlSelect . " separator ', ')")
: DbDongle::raw($sqlSelect);
$joinSql = $countQuery->select($joinSql)->toSql();
$selects[] = Db::raw("(".$joinSql.") as ".$alias);
}
/*
* Primary column
*/
else {
$sqlSelect = $this->parseTableName($column->sqlSelect, $primaryTable);
$selects[] = DbDongle::raw($sqlSelect . ' as '. $alias);
}
2014-05-14 23:24:20 +10:00
}
/*
* Apply sorting
2014-05-14 23:24:20 +10:00
*/
if ($sortColumn = $this->getSortColumn()) {
if (($column = array_get($this->allColumns, $sortColumn)) && $column->valueFrom) {
$sortColumn = $this->isColumnPivot($column)
? 'pivot_' . $column->valueFrom
: $column->valueFrom;
2014-10-11 00:39:34 +02:00
}
2014-08-23 17:35:18 +10:00
2014-05-14 23:24:20 +10:00
$query->orderBy($sortColumn, $this->sortDirection);
}
/*
2014-08-13 21:23:19 +10:00
* Apply filters
2014-05-14 23:24:20 +10:00
*/
2014-08-13 21:23:19 +10:00
foreach ($this->filterCallbacks as $callback) {
$callback($query);
}
2014-05-14 23:24:20 +10:00
2014-08-23 13:07:39 +10:00
/*
* Add custom selects
*/
$query->select($selects);
2014-05-14 23:24:20 +10:00
/*
* Extensibility
*/
if (
($event = $this->fireEvent('list.extendQuery', [$query], true)) ||
($event = Event::fire('backend.list.extendQuery', [$this, $query], true))
) {
return $event;
}
2014-05-14 23:24:20 +10:00
return $query;
}
/**
* Returns all the records from the supplied model, after filtering.
* @return Collection
*/
protected function getRecords()
{
$model = $this->prepareModel();
2014-05-14 23:24:20 +10:00
if ($this->showTree) {
$records = $model->getNested();
}
elseif ($this->showPagination) {
$records = $model->paginate($this->recordsPerPage, $this->currentPageNumber);
}
else {
$records = $model->get();
2014-05-14 23:24:20 +10:00
}
return $this->records = $records;
}
/**
* Returns the record URL address for a list row.
* @param Model $record
* @return string
*/
public function getRecordUrl($record)
{
2014-10-11 00:39:34 +02:00
if (isset($this->recordOnClick)) {
2014-05-14 23:24:20 +10:00
return 'javascript:;';
2014-10-11 00:39:34 +02:00
}
2014-05-14 23:24:20 +10:00
2014-10-11 00:39:34 +02:00
if (!isset($this->recordUrl)) {
2014-05-14 23:24:20 +10:00
return null;
2014-10-11 00:39:34 +02:00
}
2014-05-14 23:24:20 +10:00
$data = $record->toArray();
$data += [$record->getKeyName() => $record->getKey()];
$columns = array_keys($data);
$url = RouterHelper::parseValues($data, $columns, $this->recordUrl);
2014-05-14 23:24:20 +10:00
return Backend::url($url);
}
/**
* Returns the onclick event for a list row.
* @param Model $record
* @return string
*/
public function getRecordOnClick($record)
{
2014-10-11 00:39:34 +02:00
if (!isset($this->recordOnClick)) {
2014-05-14 23:24:20 +10:00
return null;
2014-10-11 00:39:34 +02:00
}
2014-05-14 23:24:20 +10:00
$columns = array_keys($record->getAttributes());
$recordOnClick = RouterHelper::parseValues($record, $columns, $this->recordOnClick);
return Html::attributes(['onclick' => $recordOnClick]);
}
2014-08-23 21:46:03 +10:00
/**
* Get all the registered columns for the instance.
* @return array
*/
public function getColumns()
{
return $this->allColumns ?: $this->defineListColumns();
2014-08-23 21:46:03 +10:00
}
/**
* Get a specified column object
* @param string $column
* @return mixed
*/
public function getColumn($column)
{
return $this->allColumns[$column];
2014-08-23 21:46:03 +10:00
}
2014-05-14 23:24:20 +10:00
/**
* Returns the list columns that are visible by list settings or default
*/
public function getVisibleColumns()
2014-05-14 23:24:20 +10:00
{
2014-08-23 21:46:03 +10:00
$definitions = $this->defineListColumns();
2014-05-14 23:24:20 +10:00
$columns = [];
/*
* Supplied column list
*/
2014-10-11 00:39:34 +02:00
if ($this->columnOverride === null) {
2014-05-14 23:24:20 +10:00
$this->columnOverride = $this->getSession('visible', null);
2014-10-11 00:39:34 +02:00
}
2014-05-14 23:24:20 +10:00
if ($this->columnOverride && is_array($this->columnOverride)) {
$invalidColumns = array_diff($this->columnOverride, array_keys($definitions));
2014-10-11 00:39:34 +02:00
if (!count($definitions)) {
throw new ApplicationException(Lang::get(
'backend::lang.list.missing_column',
['columns'=>implode(',', $invalidColumns)]
));
}
2014-05-14 23:24:20 +10:00
$availableColumns = array_intersect($this->columnOverride, array_keys($definitions));
foreach ($availableColumns as $columnName) {
2014-05-14 23:24:20 +10:00
$definitions[$columnName]->invisible = false;
$columns[$columnName] = $definitions[$columnName];
}
}
2014-05-14 23:24:20 +10:00
/*
* Use default column list
*/
else {
2014-05-14 23:24:20 +10:00
foreach ($definitions as $columnName => $column) {
2014-10-11 00:39:34 +02:00
if ($column->invisible) {
2014-05-14 23:24:20 +10:00
continue;
2014-10-11 00:39:34 +02:00
}
2014-05-14 23:24:20 +10:00
$columns[$columnName] = $definitions[$columnName];
}
}
return $this->visibleColumns = $columns;
}
/**
* Builds an array of list columns with keys as the column name and values as a ListColumn object.
*/
2014-08-23 21:46:03 +10:00
protected function defineListColumns()
2014-05-14 23:24:20 +10:00
{
if (!isset($this->columns) || !is_array($this->columns) || !count($this->columns)) {
$class = get_class($this->model instanceof Model ? $this->model : $this->controller);
throw new ApplicationException(Lang::get('backend::lang.list.missing_columns', compact('class')));
2014-10-11 00:39:34 +02:00
}
2014-05-14 23:24:20 +10:00
$this->addColumns($this->columns);
/*
* Extensibility
*/
Event::fire('backend.list.extendColumns', [$this]);
$this->fireEvent('list.extendColumns');
2014-05-14 23:24:20 +10:00
/*
* Use a supplied column order
*/
if ($columnOrder = $this->getSession('order', null)) {
$orderedDefinitions = [];
foreach ($columnOrder as $column) {
if (isset($this->allColumns[$column])) {
$orderedDefinitions[$column] = $this->allColumns[$column];
}
2014-05-14 23:24:20 +10:00
}
$this->allColumns = array_merge($orderedDefinitions, $this->allColumns);
2014-05-14 23:24:20 +10:00
}
return $this->allColumns;
}
/**
* Programatically add columns, used internally and for extensibility.
2015-05-02 10:50:19 +10:00
* @param array $columns Column definitions
*/
public function addColumns(array $columns)
{
2014-05-14 23:24:20 +10:00
/*
* Build a final collection of list column objects
*/
foreach ($columns as $columnName => $config) {
$this->allColumns[$columnName] = $this->makeListColumn($columnName, $config);
2014-05-14 23:24:20 +10:00
}
}
2015-05-01 09:09:56 +02:00
/**
* Programatically remove a column, used for extensibility.
2015-05-02 10:50:19 +10:00
* @param string $column Column name
2015-05-01 09:09:56 +02:00
*/
public function removeColumn($columnName)
{
if (isset($this->allColumns[$columnName])) {
unset($this->allColumns[$columnName]);
}
}
2014-05-14 23:24:20 +10:00
/**
* Creates a list column object from it's name and configuration.
*/
protected function makeListColumn($name, $config)
{
2014-10-11 00:39:34 +02:00
if (is_string($config)) {
2014-05-14 23:24:20 +10:00
$label = $config;
}
elseif (isset($config['label'])) {
2014-05-14 23:24:20 +10:00
$label = $config['label'];
}
else {
2014-05-14 23:24:20 +10:00
$label = studly_case($name);
2014-10-11 00:39:34 +02:00
}
/*
* Auto configure pivot relation
*/
if (starts_with($name, 'pivot[') && strpos($name, ']') !== false) {
$_name = HtmlHelper::nameToArray($name);
$relationName = array_shift($_name);
$valueFrom = array_shift($_name);
if (count($_name) > 0) {
$valueFrom .= '['.implode('][', $_name).']';
}
$config['relation'] = $relationName;
$config['valueFrom'] = $valueFrom;
$config['searchable'] = false;
}
/*
* Auto configure standard relation
*/
elseif (strpos($name, '[') !== false && strpos($name, ']') !== false) {
$config['valueFrom'] = $name;
$config['sortable'] = false;
$config['searchable'] = false;
}
2014-05-14 23:24:20 +10:00
$columnType = isset($config['type']) ? $config['type'] : null;
2014-05-14 23:24:20 +10:00
$column = new ListColumn($name, $label);
$column->displayAs($columnType, $config);
2014-05-14 23:24:20 +10:00
return $column;
}
/**
* Calculates the total columns used in the list, including checkboxes
* and other additions.
*/
protected function getTotalColumns()
{
$columns = $this->visibleColumns ?: $this->getVisibleColumns();
2014-05-14 23:24:20 +10:00
$total = count($columns);
2014-10-11 00:39:34 +02:00
if ($this->showCheckboxes) {
$total++;
}
if ($this->showSetup) {
$total++;
}
2014-05-14 23:24:20 +10:00
return $total;
}
/**
* Looks up the column header
*/
public function getHeaderValue($column)
{
$value = Lang::get($column->label);
/*
* Extensibility
*/
2014-10-11 00:39:34 +02:00
if ($response = Event::fire('backend.list.overrideHeaderValue', [$this, $column, $value], true)) {
2014-05-15 16:22:22 +10:00
$value = $response;
2014-10-11 00:39:34 +02:00
}
2014-05-14 23:24:20 +10:00
2014-10-11 00:39:34 +02:00
if ($response = $this->fireEvent('list.overrideHeaderValue', [$column, $value], true)) {
2014-05-15 17:23:46 +10:00
$value = $response;
2014-10-11 00:39:34 +02:00
}
2014-05-14 23:24:20 +10:00
return $value;
}
/**
* Returns a raw column value
* @return string
2014-05-14 23:24:20 +10:00
*/
public function getColumnValueRaw($record, $column)
2014-05-14 23:24:20 +10:00
{
$columnName = $column->columnName;
/*
* Handle taking value from model relation.
*/
if ($column->valueFrom && $column->relation) {
$columnName = $column->relation;
2014-10-11 00:39:34 +02:00
if (!array_key_exists($columnName, $record->getRelations())) {
$value = null;
}
elseif ($this->isColumnRelated($column, true)) {
$value = $record->{$columnName}->lists($column->valueFrom);
}
elseif ($this->isColumnRelated($column) || $this->isColumnPivot($column)) {
$value = $record->{$columnName}
? $column->getValueFromData($record->{$columnName})
: null;
}
else {
$value = null;
2014-10-11 00:39:34 +02:00
}
}
/*
* Handle taking value from model attribute.
*/
elseif ($column->valueFrom) {
$value = $column->getValueFromData($record);
}
/*
* Otherwise, if the column is a relation, it will be a custom select,
* so prevent the Model from attempting to load the relation
* if the value is NULL.
*/
else {
2014-10-11 00:39:34 +02:00
if ($record->hasRelation($columnName) && array_key_exists($columnName, $record->attributes)) {
$value = $record->attributes[$columnName];
}
else {
$value = $record->{$columnName};
2014-10-11 00:39:34 +02:00
}
}
2014-05-14 23:24:20 +10:00
return $value;
}
/**
* Returns a column value, with filters applied
* @return string
*/
public function getColumnValue($record, $column)
{
$value = $this->getColumnValueRaw($record, $column);
2014-10-11 00:39:34 +02:00
if (method_exists($this, 'eval'. studly_case($column->type) .'TypeValue')) {
$value = $this->{'eval'. studly_case($column->type) .'TypeValue'}($record, $column, $value);
2014-10-11 00:39:34 +02:00
}
else {
$value = $this->evalCustomListType($column->type, $record, $column, $value);
}
2014-05-14 23:24:20 +10:00
/*
* Apply default value.
*/
2015-09-03 07:15:52 +10:00
if ($value === '' || $value === null) {
$value = $column->defaults;
}
2014-05-14 23:24:20 +10:00
/*
* Extensibility
*/
2015-03-07 11:34:19 +11:00
if (($response = Event::fire('backend.list.overrideColumnValue', [$this, $record, $column, $value], true)) !== null) {
2014-05-15 16:22:22 +10:00
$value = $response;
2014-10-11 00:39:34 +02:00
}
2014-05-14 23:24:20 +10:00
2015-03-07 11:34:19 +11:00
if (($response = $this->fireEvent('list.overrideColumnValue', [$record, $column, $value], true)) !== null) {
2014-05-15 17:23:46 +10:00
$value = $response;
2014-10-11 00:39:34 +02:00
}
2014-05-14 23:24:20 +10:00
return $value;
}
/**
* Adds a custom CSS class string to a record row
* @param Model $record Populated model
* @return string
*/
public function getRowClass($record)
{
$value = '';
/*
* Extensibility
*/
2014-10-11 00:39:34 +02:00
if ($response = Event::fire('backend.list.injectRowClass', [$this, $record], true)) {
2014-05-15 16:22:22 +10:00
$value = $response;
2014-10-11 00:39:34 +02:00
}
2014-05-14 23:24:20 +10:00
2014-10-11 00:39:34 +02:00
if ($response = $this->fireEvent('list.injectRowClass', [$record], true)) {
2014-05-15 17:23:46 +10:00
$value = $response;
2014-10-11 00:39:34 +02:00
}
2014-05-14 23:24:20 +10:00
return $value;
}
//
// Value processing
//
/**
* Process a custom list types registered by plugins.
*/
protected function evalCustomListType($type, $record, $column, $value)
{
$plugins = PluginManager::instance()->getRegistrationMethodValues('registerListColumnTypes');
foreach ($plugins as $availableTypes) {
if (!isset($availableTypes[$type])) {
continue;
}
$callback = $availableTypes[$type];
if (is_callable($callback)) {
return call_user_func_array($callback, [$value, $column, $record]);
}
}
throw new ApplicationException(sprintf('List column type "%s" could not be found.', $type));
}
2014-08-01 17:42:00 +10:00
/**
* Process as text, escape the value
*/
protected function evalTextTypeValue($record, $column, $value)
{
if (is_array($value) && count($value) == count($value, COUNT_RECURSIVE)) {
$value = implode(', ', $value);
}
return htmlentities($value, ENT_QUOTES, 'UTF-8', false);
}
/**
* Process as number, proxy to text
*/
protected function evalNumberTypeValue($record, $column, $value)
{
return $this->evalTextTypeValue($record, $column, $value);
}
/**
* Common mistake, relation is not a valid list column.
* @deprecated Remove if year >= 2018
*/
protected function evalRelationTypeValue($record, $column, $value)
{
traceLog(sprintf('Warning: List column type "relation" for class "%s" is not valid.', get_class($record)));
return $this->evalTextTypeValue($record, $column, $value);
}
/**
* Process as partial reference
2014-08-01 17:42:00 +10:00
*/
protected function evalPartialTypeValue($record, $column, $value)
2014-08-01 17:42:00 +10:00
{
return $this->controller->makePartial($column->path ?: $column->columnName, [
'listColumn' => $column,
'listRecord' => $record,
'listValue' => $value,
'column' => $column,
'record' => $record,
'value' => $value
]);
2014-08-01 17:42:00 +10:00
}
2014-05-14 23:24:20 +10:00
/**
* Process as boolean switch
*/
protected function evalSwitchTypeValue($record, $column, $value)
2014-05-14 23:24:20 +10:00
{
$contents = '';
2015-10-17 08:41:10 +11:00
if ($value) {
$contents = Lang::get('backend::lang.list.column_switch_true');
}
else {
2015-10-17 08:41:10 +11:00
$contents = Lang::get('backend::lang.list.column_switch_false');
}
return $contents;
2014-05-14 23:24:20 +10:00
}
/**
* Process as a datetime value
*/
protected function evalDatetimeTypeValue($record, $column, $value)
2014-05-14 23:24:20 +10:00
{
2014-10-11 00:39:34 +02:00
if ($value === null) {
2014-05-14 23:24:20 +10:00
return null;
2014-10-11 00:39:34 +02:00
}
2014-05-14 23:24:20 +10:00
$dateTime = $this->validateDateTimeValue($value, $column);
2014-10-11 00:39:34 +02:00
if ($column->format !== null) {
$value = $dateTime->format($column->format);
}
else {
$value = $dateTime->toDayDateTimeString();
2014-10-11 00:39:34 +02:00
}
2014-05-14 23:24:20 +10:00
return Backend::dateTime($dateTime, [
'defaultValue' => $value,
'format' => $column->format,
'formatAlias' => 'dateTimeLongMin'
]);
2014-05-14 23:24:20 +10:00
}
/**
* Process as a time value
*/
protected function evalTimeTypeValue($record, $column, $value)
2014-05-14 23:24:20 +10:00
{
2014-10-11 00:39:34 +02:00
if ($value === null) {
2014-05-14 23:24:20 +10:00
return null;
2014-10-11 00:39:34 +02:00
}
2014-05-14 23:24:20 +10:00
$dateTime = $this->validateDateTimeValue($value, $column);
$format = $column->format !== null ? $column->format : 'g:i A';
2014-05-14 23:24:20 +10:00
$value = $dateTime->format($format);
return Backend::dateTime($dateTime, [
'defaultValue' => $value,
'format' => $column->format,
'formatAlias' => 'time'
]);
2014-05-14 23:24:20 +10:00
}
/**
* Process as a date value
*/
protected function evalDateTypeValue($record, $column, $value)
2014-05-14 23:24:20 +10:00
{
2014-10-11 00:39:34 +02:00
if ($value === null) {
2014-05-14 23:24:20 +10:00
return null;
2014-10-11 00:39:34 +02:00
}
2014-05-14 23:24:20 +10:00
$dateTime = $this->validateDateTimeValue($value, $column);
2014-10-11 00:39:34 +02:00
if ($column->format !== null) {
$value = $dateTime->format($column->format);
}
else {
$value = $dateTime->toFormattedDateString();
2014-10-11 00:39:34 +02:00
}
2014-05-14 23:24:20 +10:00
return Backend::dateTime($dateTime, [
'defaultValue' => $value,
'format' => $column->format,
'formatAlias' => 'dateLongMin'
]);
2014-05-14 23:24:20 +10:00
}
/**
* Process as diff for humans (1 min ago)
*/
protected function evalTimesinceTypeValue($record, $column, $value)
2014-05-14 23:24:20 +10:00
{
2014-10-11 00:39:34 +02:00
if ($value === null) {
2014-05-14 23:24:20 +10:00
return null;
2014-10-11 00:39:34 +02:00
}
$dateTime = $this->validateDateTimeValue($value, $column);
$value = DateTimeHelper::timeSince($dateTime);
return Backend::dateTime($dateTime, [
'defaultValue' => $value,
'timeSince' => true
]);
}
/**
* Process as time as current tense (Today at 0:00)
*/
protected function evalTimetenseTypeValue($record, $column, $value)
{
if ($value === null) {
return null;
}
$dateTime = $this->validateDateTimeValue($value, $column);
$value = DateTimeHelper::timeTense($dateTime);
return Backend::dateTime($dateTime, [
'defaultValue' => $value,
'timeTense' => true
]);
}
/**
* Validates a column type as a date
*/
2014-08-01 18:18:09 +10:00
protected function validateDateTimeValue($value, $column)
{
$value = DateTimeHelper::makeCarbon($value, false);
2014-10-11 00:39:34 +02:00
if (!$value instanceof Carbon) {
throw new ApplicationException(Lang::get(
'backend::lang.list.invalid_column_datetime',
['column' => $column->columnName]
));
}
return $value;
2014-05-14 23:24:20 +10:00
}
2014-08-13 21:23:19 +10:00
//
// Filtering
//
public function addFilter(callable $filter)
{
$this->filterCallbacks[] = $filter;
}
2014-05-14 23:24:20 +10:00
//
// Searching
//
/**
* Applies a search term to the list results, searching will disable tree
* view if a value is supplied.
* @param string $term
*/
public function setSearchTerm($term)
{
if (!empty($term)) {
2014-05-14 23:24:20 +10:00
$this->showTree = false;
}
$this->searchTerm = $term;
}
/**
* Applies a search options to the list search.
* @param array $options
*/
public function setSearchOptions($options = [])
{
extract(array_merge([
'mode' => null,
'scope' => null
], $options));
$this->searchMode = $mode;
$this->searchScope = $scope;
}
2014-05-14 23:24:20 +10:00
/**
* Returns a collection of columns which can be searched.
* @return array
*/
protected function getSearchableColumns()
{
2014-08-23 21:46:03 +10:00
$columns = $this->getColumns();
2014-05-14 23:24:20 +10:00
$searchable = [];
foreach ($columns as $column) {
2014-10-11 00:39:34 +02:00
if (!$column->searchable) {
2014-05-14 23:24:20 +10:00
continue;
2014-10-11 00:39:34 +02:00
}
2014-05-14 23:24:20 +10:00
$searchable[] = $column;
}
return $searchable;
}
/**
* Applies the search constraint to a query.
*/
protected function applySearchToQuery($query, $columns, $boolean = 'and')
{
$term = $this->searchTerm;
if ($scopeMethod = $this->searchScope) {
2016-04-20 05:29:24 +10:00
$searchMethod = $boolean == 'and' ? 'where' : 'orWhere';
$query->$searchMethod(function($q) use ($term, $columns, $scopeMethod) {
$q->$scopeMethod($term, $columns);
2016-04-20 05:29:24 +10:00
});
}
else {
$searchMethod = $boolean == 'and' ? 'searchWhere' : 'orSearchWhere';
$query->$searchMethod($term, $columns, $this->searchMode);
}
}
2014-05-14 23:24:20 +10:00
//
// Sorting
//
/**
* Event handler for sorting the list.
*/
public function onSort()
{
if ($column = post('sortColumn')) {
/*
* Toggle the sort direction and set the sorting column
*/
$sortOptions = ['column' => $this->getSortColumn(), 'direction' => $this->sortDirection];
2014-10-11 00:39:34 +02:00
if ($column != $sortOptions['column'] || $sortOptions['direction'] == 'asc') {
2014-05-14 23:24:20 +10:00
$this->sortDirection = $sortOptions['direction'] = 'desc';
}
else {
2014-05-14 23:24:20 +10:00
$this->sortDirection = $sortOptions['direction'] = 'asc';
2014-10-11 00:39:34 +02:00
}
2014-05-14 23:24:20 +10:00
$this->sortColumn = $sortOptions['column'] = $column;
$this->putSession('sort', $sortOptions);
/*
* Persist the page number
*/
$this->currentPageNumber = post('page');
2014-05-14 23:24:20 +10:00
return $this->onRefresh();
2014-05-14 23:24:20 +10:00
}
}
/**
* Returns the current sorting column, saved in a session or cached.
*/
protected function getSortColumn()
{
2014-10-11 00:39:34 +02:00
if (!$this->isSortable()) {
2014-05-14 23:24:20 +10:00
return false;
2014-10-11 00:39:34 +02:00
}
2014-05-14 23:24:20 +10:00
2014-10-11 00:39:34 +02:00
if ($this->sortColumn !== null) {
2014-05-14 23:24:20 +10:00
return $this->sortColumn;
2014-10-11 00:39:34 +02:00
}
2014-05-14 23:24:20 +10:00
/*
* User preference
*/
if ($this->showSorting && ($sortOptions = $this->getSession('sort'))) {
$this->sortColumn = $sortOptions['column'];
$this->sortDirection = $sortOptions['direction'];
}
else {
2014-11-01 12:03:38 +11:00
/*
* Supplied default
*/
2014-05-14 23:24:20 +10:00
if (is_string($this->defaultSort)) {
$this->sortColumn = $this->defaultSort;
$this->sortDirection = 'desc';
}
elseif (is_array($this->defaultSort) && isset($this->defaultSort['column'])) {
2014-05-14 23:24:20 +10:00
$this->sortColumn = $this->defaultSort['column'];
2014-10-11 00:39:34 +02:00
$this->sortDirection = (isset($this->defaultSort['direction'])) ?
$this->defaultSort['direction'] :
'desc';
2014-05-14 23:24:20 +10:00
}
}
/*
* First available column
*/
if ($this->sortColumn === null || !$this->isSortable($this->sortColumn)) {
$columns = $this->visibleColumns ?: $this->getVisibleColumns();
$columns = array_filter($columns, function($column){ return $column->sortable; });
2014-05-14 23:24:20 +10:00
$this->sortColumn = key($columns);
$this->sortDirection = 'desc';
}
return $this->sortColumn;
}
/**
* Returns true if the column can be sorted.
*/
protected function isSortable($column = null)
{
2014-10-11 00:39:34 +02:00
if ($column === null) {
2014-05-14 23:24:20 +10:00
return (count($this->getSortableColumns()) > 0);
}
else {
2014-05-14 23:24:20 +10:00
return array_key_exists($column, $this->getSortableColumns());
2014-10-11 00:39:34 +02:00
}
2014-05-14 23:24:20 +10:00
}
/**
* Returns a collection of columns which are sortable.
*/
protected function getSortableColumns()
{
2014-10-11 00:39:34 +02:00
if ($this->sortableColumns !== null) {
2014-05-14 23:24:20 +10:00
return $this->sortableColumns;
2014-10-11 00:39:34 +02:00
}
2014-05-14 23:24:20 +10:00
2014-08-23 21:46:03 +10:00
$columns = $this->getColumns();
$sortable = array_filter($columns, function($column){
return $column->sortable;
});
2014-05-14 23:24:20 +10:00
return $this->sortableColumns = $sortable;
}
//
// List Setup
//
/**
* Event handler to display the list set up.
*/
public function onLoadSetup()
{
$this->vars['columns'] = $this->getSetupListColumns();
$this->vars['perPageOptions'] = $this->getSetupPerPageOptions();
$this->vars['recordsPerPage'] = $this->recordsPerPage;
return $this->makePartial('setup_form');
}
/**
* Event handler to apply the list set up.
*/
public function onApplySetup()
{
if (($visibleColumns = post('visible_columns')) && is_array($visibleColumns)) {
$this->columnOverride = array_keys($visibleColumns);
$this->putSession('visible', $this->columnOverride);
2014-05-14 23:24:20 +10:00
}
$this->recordsPerPage = post('records_per_page', $this->recordsPerPage);
2014-05-14 23:24:20 +10:00
$this->putSession('order', post('column_order'));
$this->putSession('per_page', $this->recordsPerPage);
return $this->onRefresh();
2014-05-14 23:24:20 +10:00
}
/**
* Returns an array of allowable records per page.
*/
protected function getSetupPerPageOptions()
{
$perPageOptions = [20, 40, 80, 100, 120];
2014-10-11 00:39:34 +02:00
if (!in_array($this->recordsPerPage, $perPageOptions)) {
2014-05-14 23:24:20 +10:00
$perPageOptions[] = $this->recordsPerPage;
2014-10-11 00:39:34 +02:00
}
2014-05-14 23:24:20 +10:00
sort($perPageOptions);
return $perPageOptions;
}
/**
* Returns all the list columns used for list set up.
*/
protected function getSetupListColumns()
{
/*
* Force all columns invisible
*/
2014-08-23 21:46:03 +10:00
$columns = $this->defineListColumns();
foreach ($columns as $column) {
2014-05-14 23:24:20 +10:00
$column->invisible = true;
}
return array_merge($columns, $this->getVisibleColumns());
2014-05-14 23:24:20 +10:00
}
//
// Tree
//
/**
* Validates the model and settings if showTree is used
* @return void
*/
public function validateTree()
{
2014-10-11 00:39:34 +02:00
if (!$this->showTree) {
return;
}
2014-05-14 23:24:20 +10:00
$this->showSorting = $this->showPagination = false;
2014-10-11 00:39:34 +02:00
if (!$this->model->methodExists('getChildren')) {
throw new ApplicationException(
'To display list as a tree, the specified model must have a method "getChildren"'
);
}
2014-05-14 23:24:20 +10:00
2014-10-11 00:39:34 +02:00
if (!$this->model->methodExists('getChildCount')) {
throw new ApplicationException(
'To display list as a tree, the specified model must have a method "getChildCount"'
);
}
2014-05-14 23:24:20 +10:00
}
/**
* Checks if a node (model) is expanded in the session.
* @param Model $node
* @return boolean
*/
public function isTreeNodeExpanded($node)
{
return $this->getSession('tree_node_status_' . $node->getKey(), $this->treeExpanded);
}
/**
* Sets a node (model) to an expanded or collapsed state, stored in the
* session, then renders the list again.
* @return string List HTML contents.
*/
public function onToggleTreeNode()
{
$this->putSession('tree_node_status_' . post('node_id'), post('status') ? 0 : 1);
return $this->onRefresh();
2014-05-14 23:24:20 +10:00
}
//
// Helpers
//
2014-08-23 17:35:18 +10:00
/**
* Check if column refers to a relation of the model
* @param ListColumn $column List column object
* @param boolean $multi If set, returns true only if the relation is a "multiple relation type"
* @return boolean
*/
protected function isColumnRelated($column, $multi = false)
{
if (!isset($column->relation) || $this->isColumnPivot($column)) {
return false;
2014-10-11 00:39:34 +02:00
}
2014-10-11 00:39:34 +02:00
if (!$this->model->hasRelation($column->relation)) {
throw new ApplicationException(Lang::get(
'backend::lang.model.missing_relation',
['class'=>get_class($this->model), 'relation'=>$column->relation]
));
}
2014-10-11 00:39:34 +02:00
if (!$multi) {
return true;
2014-10-11 00:39:34 +02:00
}
$relationType = $this->model->getRelationType($column->relation);
return in_array($relationType, [
'hasMany',
'belongsToMany',
'morphToMany',
'morphedByMany',
'morphMany',
'attachMany',
'hasManyThrough'
]);
}
/**
* Checks if a column refers to a pivot model specifically.
* @param ListColumn $column List column object
* @return boolean
*/
protected function isColumnPivot($column)
{
if (!isset($column->relation) || $column->relation != 'pivot') {
return false;
}
return true;
}
2014-10-11 00:39:34 +02:00
}