Merge branch 'develop'

This commit is contained in:
Sam Georges 2014-08-19 12:38:25 +10:00
commit d7f22dc797
11 changed files with 228 additions and 71 deletions

View File

@ -1,3 +1,7 @@
* **Build 13x** (2014-08-xx)
- List widget has been refactored to improve efficiency.
- Added new list column type `nameFrom` (take name from X attribute) as an alternative to `select`.
* **Build 137** (2014-08-14)
- Lists now support Filters (see Backend > Lists docs).
- Numerous hard coded phrases converted to localized strings.

View File

@ -41,7 +41,14 @@ class ListColumn
public $sortable = true;
/**
* @var string Custom SQL for selecting this record value.
* @var string Model attribute to use for the display value, this will
* override any $sqlSelect definition.
*/
public $nameFrom;
/**
* @var string Custom SQL for selecting this record display value,
* the @ symbol is replaced with the table name.
*/
public $sqlSelect;
@ -103,6 +110,7 @@ class ListColumn
if (isset($config['searchable'])) $this->searchable = $config['searchable'];
if (isset($config['sortable'])) $this->sortable = $config['sortable'];
if (isset($config['invisible'])) $this->invisible = $config['invisible'];
if (isset($config['nameFrom'])) $this->nameFrom = $config['nameFrom'];
if (isset($config['select'])) $this->sqlSelect = $config['select'];
if (isset($config['relation'])) $this->relation = $config['relation'];
if (isset($config['format'])) $this->format = $config['format'];

View File

@ -47,6 +47,7 @@ return [
'menu_label' => 'Dashboard',
'widget_label' => 'Widget',
'widget_width' => 'Width',
'full_width' => 'full width',
'add_widget' => 'Add widget',
'widget_inspector_title' => 'Widget configuration',
'widget_inspector_description' => 'Configure the report widget',
@ -119,15 +120,13 @@ return [
'setup_title' => 'List Setup',
'setup_help' => 'Use checkboxes to select columns you want to see in the list. You can change position of columns by dragging them up or down.',
'records_per_page' => 'Records per page',
'records_per_page_help' => 'Select the number of records per page to display. Please note that high number of records on a single page can reduce performance.',
'apply_changes' => 'Apply changes',
'cancel' => 'Cancel'
'records_per_page_help' => 'Select the number of records per page to display. Please note that high number of records on a single page can reduce performance.'
],
'fileupload' => [
'attachment' => 'Attachment',
'help' => 'Add a title and description for this attachment.',
'title_label' => 'Title',
'description_label' => 'Description'
'attachment' => 'Attachment',
'help' => 'Add a title and description for this attachment.',
'title_label' => 'Title',
'description_label' => 'Description'
],
'form' => [
'create_title' => "New :name",
@ -163,6 +162,7 @@ return [
'select' => 'Select',
'select_all' => 'all',
'select_none' => 'none',
'select_placeholder' => 'please select',
'insert_row' => 'Insert Row',
'delete_row' => 'Delete Row'
],

View File

@ -113,9 +113,7 @@ return [
'setup_title' => 'Configura elenco',
'setup_help' => 'Utilizza le checkbox per selezionare le colonne che vuoi visualizzare nell\'elenco. Puoi cambiare la posizione delle colonne trascinandole verso l\'alto o il basso.',
'records_per_page' => 'Record per pagina',
'records_per_page_help' => 'Seleziona il numero di record da visualizzare su ogni pagina. Ricorda che un numero elevato di record in una singola pagina può ridurre le prestazioni.',
'apply_changes' => 'Applica modifiche',
'cancel' => 'Annulla'
'records_per_page_help' => 'Seleziona il numero di record da visualizzare su ogni pagina. Ricorda che un numero elevato di record in una singola pagina può ridurre le prestazioni.'
],
'form' => [
'create_title' => "Nuovo :name",

View File

@ -265,9 +265,10 @@ class Lists extends WidgetBase
protected function prepareModel()
{
$query = $this->model->newQuery();
$selects = [$this->model->getTable().'.*'];
$tables = ['base'=>$this->model->getTable()];
$primaryTable = $this->model->getTable();
$selects = [$primaryTable.'.*'];
$joins = [];
$withs = [];
/*
* Extensibility
@ -276,68 +277,129 @@ class Lists extends WidgetBase
$this->fireEvent('list.extendQueryBefore', [$query]);
/*
* Related custom selects, must come first
* Prepare searchable column names
*/
foreach ($this->getVisibleListColumns() as $column) {
if (!isset($column->relation) || !isset($column->sqlSelect))
continue;
$primarySearchable = [];
$relationSearchable = [];
if (!$this->model->hasRelation($column->relation))
throw new ApplicationException(Lang::get('backend::lang.model.missing_relation', ['class'=>get_class($this->model), 'relation'=>$column->relation]));
$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))
: $table . '.' . $column->nameFrom;
$alias = Db::getQueryGrammar()->wrap($column->columnName);
$table = $this->model->makeRelation($column->relation)->getTable();
$relationType = $this->model->getRelationType($column->relation);
$sqlSelect = $this->parseTableName($column->sqlSelect, $table);
$relationSearchable[$column->relation][] = $columnName;
}
/*
* Primary
*/
else {
$columnName = isset($column->sqlSelect)
? DbDongle::raw($this->parseTableName($column->sqlSelect, $primaryTable))
: $primaryTable . '.' . $column->columnName;
if (in_array($relationType, ['hasMany', 'belongsToMany', 'morphToMany', 'morphedByMany', 'morphMany', 'attachMany', 'hasManyThrough']))
$selects[] = DbDongle::raw("group_concat(" . $sqlSelect . " separator ', ') as ". $alias);
else
$selects[] = DbDongle::raw($sqlSelect . ' as '. $alias);
$joins[] = $column->relation;
$tables[$column->relation] = $table;
$primarySearchable[] = $columnName;
}
}
}
if ($joins)
$query->joinWith(array_unique($joins), false);
/*
* Prepare related eager loads (withs) and custom selects (joins)
*/
foreach ($this->getVisibleListColumns() as $column) {
if (!$this->isColumnRelated($column) || (!isset($column->sqlSelect) && !isset($column->nameFrom)))
continue;
if (isset($column->nameFrom))
$withs[] = $column->relation;
$joins[] = $column->relation;
}
/*
* Include any relation constraints
*/
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) {
$query->whereHas($join, function($_query) use ($columnsToSearch) {
$_query->searchWhere($this->searchTerm, $columnsToSearch);
});
}
}
}
/*
* Add eager loads to the query
*/
if ($withs) {
$query->with(array_unique($withs));
}
/*
* Custom select queries
*/
foreach ($this->getVisibleListColumns() as $column) {
if (!isset($column->sqlSelect) || isset($column->relation))
if (!isset($column->sqlSelect))
continue;
$alias = Db::getQueryGrammar()->wrap($column->columnName);
$sqlSelect = $this->parseTableName($column->sqlSelect, $tables['base']);
$selects[] = DbDongle::raw($sqlSelect . ' as '. $alias);
/*
* Relation column
*/
if (isset($column->relation)) {
$table = $this->model->makeRelation($column->relation)->getTable();
$relationType = $this->model->getRelationType($column->relation);
$sqlSelect = $this->parseTableName($column->sqlSelect, $table);
/*
* Manipulate a count query for the sub query
*/
$relationObj = $this->model->{$column->relation}();
$countQuery = $relationObj->getRelationCountQuery($relationObj->getRelated()->newQuery(), $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);
}
}
/*
* Handle a supplied search term
* Apply a supplied search term for primary columns
*/
if (!empty($this->searchTerm) && ($searchableColumns = $this->getSearchableColumns())) {
$query->orWhere(function($innerQuery) use ($searchableColumns, $tables) {
$columnsToSearch = [];
foreach ($searchableColumns as $column) {
if (isset($column->sqlSelect)) {
$table = (isset($column->relation)) ? $tables[$column->relation] : 'base';
$columnName = DbDongle::raw($this->parseTableName($column->sqlSelect, $table));
}
else
$columnName = $tables['base'] . '.' . $column->columnName;
$columnsToSearch[] = $columnName;
}
$innerQuery->searchWhere($this->searchTerm, $columnsToSearch);
if (count($primarySearchable) > 0) {
$query->orWhere(function($innerQuery) use ($primarySearchable) {
$innerQuery->searchWhere($this->searchTerm, $primarySearchable);
});
}
/*
* Handle sorting
* Apply sorting
*/
if ($sortColumn = $this->getSortColumn()) {
$query->orderBy($sortColumn, $this->sortDirection);
@ -356,9 +418,7 @@ class Lists extends WidgetBase
Event::fire('backend.list.extendQuery', [$this, $query]);
$this->fireEvent('list.extendQuery', [$query]);
// Grouping due to the joinWith() call
$query->select($selects);
$query->groupBy($this->model->getQualifiedKeyName());
return $query;
}
@ -555,16 +615,33 @@ class Lists extends WidgetBase
*/
public function getColumnValue($record, $column)
{
$columnName = $column->columnName;
/*
* If the column is a relation, it will be a custom select,
* Handle taking name from model attribute.
*/
if ($column->nameFrom) {
if (!array_key_exists($columnName, $record->getRelations()))
$value = null;
elseif ($this->isColumnRelated($column, true))
$value = implode(', ', $record->{$columnName}->lists($column->nameFrom));
elseif ($this->isColumnRelated($column))
$value = $record->{$columnName}->{$column->nameFrom};
else
$value = $record->{$column->nameFrom};
}
/*
* 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.
*/
$columnName = $column->columnName;
if ($record->hasRelation($columnName) && array_key_exists($columnName, $record->attributes))
$value = $record->attributes[$columnName];
else
$value = $record->{$columnName};
else {
if ($record->hasRelation($columnName) && array_key_exists($columnName, $record->attributes))
$value = $record->attributes[$columnName];
else
$value = $record->{$columnName};
}
if (method_exists($this, 'eval'. studly_case($column->type) .'TypeValue'))
$value = $this->{'eval'. studly_case($column->type) .'TypeValue'}($record, $column, $value);
@ -962,4 +1039,38 @@ class Lists extends WidgetBase
return $this->onRefresh();
}
//
// Helpers
//
/**
* 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))
return false;
if (!$this->model->hasRelation($column->relation))
throw new ApplicationException(Lang::get('backend::lang.model.missing_relation', ['class'=>get_class($this->model), 'relation'=>$column->relation]));
if (!$multi)
return true;
$relationType = $this->model->getRelationType($column->relation);
return in_array($relationType, [
'hasMany',
'belongsToMany',
'morphToMany',
'morphedByMany',
'morphMany',
'attachMany',
'hasManyThrough'
]);
}
}

View File

@ -117,7 +117,7 @@ class ReportContainer extends WidgetBase
{
$sizes = [];
for ($i = 1; $i <= 10; $i++)
$sizes[$i] = $i < 10 ? $i : $i.' (full width)';
$sizes[$i] = $i < 10 ? $i : $i.' (' . Lang::get('backend::lang.dashboard.full_width') . ')';
$this->vars['sizes'] = $sizes;
$this->vars['widgets'] = WidgetManager::instance()->listReportWidgets();

View File

@ -54,13 +54,13 @@
data-request="<?= $this->getEventHandler('onApplySetup') ?>"
data-dismiss="popup"
data-stripe-load-indicator>
<?= e(trans('backend::lang.list.apply_changes')) ?>
<?= e(trans('backend::lang.form.apply')) ?>
</button>
<button
type="button"
class="btn btn-default"
data-dismiss="popup">
<?= e(trans('backend::lang.list.cancel')) ?>
<?= e(trans('backend::lang.form.cancel')) ?>
</button>
</div>
<?= Form::close() ?>

View File

@ -10,7 +10,7 @@
<div class="modal-body">
<div class="form-group">
<label><?= e(trans('backend::lang.dashboard.widget_label')) ?></label>
<select class="form-control custom-select" name="className" data-placeholder="please select">
<select class="form-control custom-select" name="className" data-placeholder="<?= e(trans('backend::lang.form.select_placeholder')) ?>">
<option></option>
<?php foreach ($widgets as $className => $widgetInfo):?>
<option value="<?= e($className) ?>"><?= isset($widgetInfo['label']) ? e(trans($widgetInfo['label'])) : $className ?></option>

View File

@ -255,7 +255,10 @@ class Controller extends BaseController
{
$this->loader = new TwigLoader();
$options = ['auto_reload' => true];
$options = [
'auto_reload' => true,
'debug' => Config::get('app.debug', false),
];
if (!Config::get('cms.twigNoCache'))
$options['cache'] = storage_path().'/twig';
@ -548,7 +551,18 @@ class Controller extends BaseController
*/
public function renderPage()
{
return $this->pageContents;
$contents = $this->pageContents;
/*
* Extensibility
*/
if ($event = $this->fireEvent('page.render', [$contents], true))
return $event;
if ($event = Event::fire('cms.page.render', [$this, $contents], true))
return $event;
return $contents;
}
/**

View File

@ -45,7 +45,7 @@ class RequestLogs extends Controller
public function onEmptyLog()
{
RequestLog::truncate();
Flash::success(Lang::get('system::lang.event_log.empty_success'));
Flash::success(Lang::get('system::lang.request_log.empty_success'));
return $this->listRefresh();
}

View File

@ -38,13 +38,21 @@ trait AssetMaker
if ($type != null) $type = strtolower($type);
$result = null;
$reserved = ['build'];
$pathCache = [];
if ($type == null || $type == 'css'){
foreach ($this->assets['css'] as $asset) {
/*
* Prevent duplicates
*/
$path = $this->getAssetEntryBuildPath($asset);
if (isset($pathCache[$path])) continue;
$pathCache[$path] = true;
$attributes = HTML::attributes(array_merge([
'rel' => 'stylesheet',
'href' => $this->getAssetEntryBuildPath($asset)
'href' => $path
],
array_except($asset['attributes'], $reserved)
));
@ -56,9 +64,16 @@ trait AssetMaker
if ($type == null || $type == 'rss'){
foreach ($this->assets['rss'] as $asset) {
/*
* Prevent duplicates
*/
$path = $this->getAssetEntryBuildPath($asset);
if (isset($pathCache[$path])) continue;
$pathCache[$path] = true;
$attributes = HTML::attributes(array_merge([
'rel' => 'alternate',
'href' => $this->getAssetEntryBuildPath($asset),
'href' => $path,
'title' => 'RSS',
'type' => 'application/rss+xml'
],
@ -72,8 +87,15 @@ trait AssetMaker
if ($type == null || $type == 'js') {
foreach ($this->assets['js'] as $asset) {
/*
* Prevent duplicates
*/
$path = $this->getAssetEntryBuildPath($asset);
if (isset($pathCache[$path])) continue;
$pathCache[$path] = true;
$attributes = HTML::attributes(array_merge([
'src' => $this->getAssetEntryBuildPath($asset)
'src' => $path
],
array_except($asset['attributes'], $reserved)
));