From f6fc9a9e00d986a748787ef0e63ffe94c414ea82 Mon Sep 17 00:00:00 2001 From: Sam Georges Date: Thu, 14 Aug 2014 19:32:48 +1000 Subject: [PATCH 01/29] YAML properties should be camelCase --- modules/backend/behaviors/FormController.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/backend/behaviors/FormController.php b/modules/backend/behaviors/FormController.php index b2c8e5cc8..0d5a8a9fb 100644 --- a/modules/backend/behaviors/FormController.php +++ b/modules/backend/behaviors/FormController.php @@ -154,7 +154,7 @@ class FormController extends ControllerBehavior $this->controller->formAfterSave($model); $this->controller->formAfterCreate($model); - Flash::success($this->getLang('create[flash-save]', 'backend::lang.form.create_success')); + Flash::success($this->getLang('create[flashSave]', 'backend::lang.form.create_success')); if ($redirect = $this->makeRedirect('create', $model)) return $redirect; @@ -207,7 +207,7 @@ class FormController extends ControllerBehavior $this->controller->formAfterSave($model); $this->controller->formAfterUpdate($model); - Flash::success($this->getLang('update[flash-save]', 'backend::lang.form.update_success')); + Flash::success($this->getLang('update[flashSave]', 'backend::lang.form.update_success')); if ($redirect = $this->makeRedirect('update', $model)) return $redirect; @@ -228,7 +228,7 @@ class FormController extends ControllerBehavior $this->controller->formAfterDelete($model); - Flash::success($this->getLang('update[flash-delete]', 'backend::lang.form.delete_success')); + Flash::success($this->getLang('update[flashDelete]', 'backend::lang.form.delete_success')); if ($redirect = $this->makeRedirect('delete', $model)) return $redirect; From e4ee8fdc4676024fb6505e831ab69dc9d7a8ffd1 Mon Sep 17 00:00:00 2001 From: Sam Georges Date: Thu, 14 Aug 2014 19:38:02 +1000 Subject: [PATCH 02/29] * Build 137 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 196e26d63..61e055f65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +* **Build 137** (2014-08-14) + - Lists now support Filters (see Backend > Lists docs). + - Numerous hard coded phrases converted to localized strings. + * **Build 132** (2014-08-03) - New system logging pages: Event log, Request log and Access log. From 869765af76cc8379427a59908fe1d5f3c40d2deb Mon Sep 17 00:00:00 2001 From: flynsarmy Date: Thu, 14 Aug 2014 21:06:58 +1000 Subject: [PATCH 03/29] Set twigs debug state to the same as Octobers --- modules/cms/classes/Controller.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/cms/classes/Controller.php b/modules/cms/classes/Controller.php index 045fb75a6..fb5c5de5a 100644 --- a/modules/cms/classes/Controller.php +++ b/modules/cms/classes/Controller.php @@ -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'; From 35e4f9c4fba26efd6359b15139a1b54df9af76f2 Mon Sep 17 00:00:00 2001 From: flynsarmy Date: Thu, 14 Aug 2014 21:08:15 +1000 Subject: [PATCH 04/29] Tailing comma --- modules/cms/classes/Controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/cms/classes/Controller.php b/modules/cms/classes/Controller.php index fb5c5de5a..7ca9cd5a2 100644 --- a/modules/cms/classes/Controller.php +++ b/modules/cms/classes/Controller.php @@ -257,7 +257,7 @@ class Controller extends BaseController $options = [ 'auto_reload' => true, - 'debug' => Config::get('app.debug', false) + 'debug' => Config::get('app.debug', false), ]; if (!Config::get('cms.twigNoCache')) $options['cache'] = storage_path().'/twig'; From 982b22a676c258d641ee2c577ab514e5edb9d84a Mon Sep 17 00:00:00 2001 From: Sam Georges Date: Sat, 16 Aug 2014 09:17:09 +1000 Subject: [PATCH 05/29] Fixes #547 - Duplicate assets are pointless --- modules/system/traits/AssetMaker.php | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/modules/system/traits/AssetMaker.php b/modules/system/traits/AssetMaker.php index 6e5be3450..bd5c95c30 100644 --- a/modules/system/traits/AssetMaker.php +++ b/modules/system/traits/AssetMaker.php @@ -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) )); From d52fe388db88f78e396b52f8465fa4829c123141 Mon Sep 17 00:00:00 2001 From: Sam Georges Date: Sat, 16 Aug 2014 14:08:51 +1000 Subject: [PATCH 06/29] Fixes #530 - No more joinWith() and groupBy(), align with Laravel to the best of our ability. Adds new list column type "nameFrom" (take name from X attribute) as an alternative to "select". --- modules/backend/classes/ListColumn.php | 10 +- modules/backend/widgets/Lists.php | 211 +++++++++++++++++++------ 2 files changed, 170 insertions(+), 51 deletions(-) diff --git a/modules/backend/classes/ListColumn.php b/modules/backend/classes/ListColumn.php index ea9d14346..7cc7ff096 100644 --- a/modules/backend/classes/ListColumn.php +++ b/modules/backend/classes/ListColumn.php @@ -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']; diff --git a/modules/backend/widgets/Lists.php b/modules/backend/widgets/Lists.php index a0cce838b..8c8216b73 100644 --- a/modules/backend/widgets/Lists.php +++ b/modules/backend/widgets/Lists.php @@ -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' + ]); + } + } \ No newline at end of file From 96926256c25a59bb1e1f1d45969626c99356e46d Mon Sep 17 00:00:00 2001 From: Sam Georges Date: Sat, 16 Aug 2014 14:10:26 +1000 Subject: [PATCH 07/29] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61e055f65..13a85cc29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. From 2c46036d2c7f062974cf5b9d68521ef62d6c9372 Mon Sep 17 00:00:00 2001 From: Paul Wilde Date: Sat, 16 Aug 2014 05:18:37 +0100 Subject: [PATCH 08/29] Added a couple of missing translation strings. Fixed a translation typo for the request log empty button - It should be request_log not event_log. --- modules/backend/lang/en/lang.php | 2 ++ modules/backend/widgets/ReportContainer.php | 2 +- .../widgets/reportcontainer/partials/_new_widget_popup.htm | 2 +- modules/system/controllers/RequestLogs.php | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/modules/backend/lang/en/lang.php b/modules/backend/lang/en/lang.php index e6483cc11..d631351c6 100644 --- a/modules/backend/lang/en/lang.php +++ b/modules/backend/lang/en/lang.php @@ -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', @@ -163,6 +164,7 @@ return [ 'select' => 'Select', 'select_all' => 'all', 'select_none' => 'none', + 'select_placeholder' => 'please select', 'insert_row' => 'Insert Row', 'delete_row' => 'Delete Row' ], diff --git a/modules/backend/widgets/ReportContainer.php b/modules/backend/widgets/ReportContainer.php index 623649777..161048c88 100644 --- a/modules/backend/widgets/ReportContainer.php +++ b/modules/backend/widgets/ReportContainer.php @@ -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(); diff --git a/modules/backend/widgets/reportcontainer/partials/_new_widget_popup.htm b/modules/backend/widgets/reportcontainer/partials/_new_widget_popup.htm index 6121d674d..e27c7410b 100644 --- a/modules/backend/widgets/reportcontainer/partials/_new_widget_popup.htm +++ b/modules/backend/widgets/reportcontainer/partials/_new_widget_popup.htm @@ -10,7 +10,7 @@