Browse Source

Merge branch 'master' into xml-formatter-root-and-object-tags

tags/2.0.11
Alexander Makarov 8 years ago committed by GitHub
parent
commit
c175c6b73a
  1. 2
      .gitignore
  2. 8
      .travis.yml
  3. 7
      docs/guide/db-active-record.md
  4. 20
      docs/guide/db-dao.md
  5. BIN
      docs/guide/images/start-gii-crud-preview.png
  6. BIN
      docs/guide/images/start-gii-crud.png
  7. BIN
      docs/guide/images/start-gii-model-preview.png
  8. BIN
      docs/guide/images/start-gii-model.png
  9. 3
      docs/guide/input-validation.md
  10. 203
      docs/guide/output-client-scripts.md
  11. 35
      docs/guide/rest-resources.md
  12. 2
      docs/guide/runtime-responses.md
  13. 1
      docs/guide/structure-assets.md
  14. 27
      framework/CHANGELOG.md
  15. 10
      framework/UPGRADE.md
  16. 80
      framework/assets/yii.gridView.js
  17. 9
      framework/assets/yii.js
  18. 2
      framework/caching/ArrayCache.php
  19. 4
      framework/caching/FileDependency.php
  20. 2
      framework/composer.json
  21. 4
      framework/console/Request.php
  22. 1
      framework/data/ActiveDataProvider.php
  23. 9
      framework/db/ActiveRecord.php
  24. 17
      framework/db/Connection.php
  25. 27
      framework/db/Migration.php
  26. 10
      framework/db/Query.php
  27. 7
      framework/db/Transaction.php
  28. 3
      framework/db/pgsql/Schema.php
  29. 2
      framework/di/ServiceLocator.php
  30. 2
      framework/helpers/BaseFileHelper.php
  31. 9
      framework/helpers/BaseJson.php
  32. 21
      framework/i18n/Formatter.php
  33. 23
      framework/messages/fa/yii.php
  34. 12
      framework/rbac/BaseManager.php
  35. 5
      framework/rbac/DbManager.php
  36. 5
      framework/rbac/PhpManager.php
  37. 2
      framework/rest/UrlRule.php
  38. 2
      framework/validators/InlineValidator.php
  39. 83
      framework/widgets/ListView.php
  40. 7
      framework/widgets/Menu.php
  41. 9
      framework/widgets/Pjax.php
  42. 5
      tests/data/travis/imagick-setup.sh
  43. 63
      tests/framework/console/RequestTest.php
  44. 44
      tests/framework/console/controllers/MigrateControllerTestTrait.php
  45. 63
      tests/framework/db/QueryBuilderTest.php
  46. 8
      tests/framework/db/QueryTest.php
  47. 11
      tests/framework/db/mssql/QueryBuilderTest.php
  48. 11
      tests/framework/db/pgsql/QueryBuilderTest.php
  49. 25
      tests/framework/di/ServiceLocatorTest.php
  50. 26
      tests/framework/helpers/ArrayHelperTest.php
  51. 59
      tests/framework/helpers/FileHelperTest.php
  52. 10
      tests/framework/rbac/ManagerTestCase.php
  53. 124
      tests/framework/rest/UrlRuleTest.php
  54. 10
      tests/framework/validators/CompareValidatorTest.php
  55. 8
      tests/framework/widgets/ActiveFieldTest.php
  56. 33
      tests/framework/widgets/ListViewTest.php
  57. 41
      tests/framework/widgets/MenuTest.php
  58. 37
      tests/framework/widgets/PjaxTest.php
  59. 170
      tests/js/data/yii.gridView.html
  60. 754
      tests/js/tests/yii.gridView.test.js

2
.gitignore vendored

@ -35,7 +35,7 @@ phpunit.phar
# local phpunit config
/phpunit.xml
# ignore sub directory for dev installed apps and extensions
# ignore dev installed apps and extensions
/apps
/extensions

8
.travis.yml

@ -72,7 +72,7 @@ matrix:
- phpenv config-rm xdebug.ini || echo "xdebug is not installed"
- travis_retry composer self-update && composer --version
- travis_retry composer global require "fxp/composer-asset-plugin:^1.2.0" --no-plugins
- travis_retry composer update --prefer-dist --no-interaction
- travis_retry composer install --prefer-dist --no-interaction
before_script:
- node --version
- npm --version
@ -111,12 +111,14 @@ install:
- travis_retry composer global require "fxp/composer-asset-plugin:^1.2.0" --no-plugins
- export PATH="$HOME/.composer/vendor/bin:$PATH"
# core framework:
- travis_retry composer update --prefer-dist --no-interaction
- travis_retry composer install --prefer-dist --no-interaction
- tests/data/travis/apc-setup.sh
- tests/data/travis/memcache-setup.sh
# - tests/data/travis/cubrid-setup.sh
- tests/data/travis/imagick-setup.sh
before_script:
# Disable the HHVM JIT for faster Unit Testing
- if [[ $TRAVIS_PHP_VERSION = hhv* ]]; then echo 'hhvm.jit = 0' >> /etc/hhvm/php.ini; fi
# show some versions and env information
- php -r "echo INTL_ICU_VERSION . \"\n\";"
- php -r "echo INTL_ICU_DATA_VERSION . \"\n\";"

7
docs/guide/db-active-record.md

@ -636,9 +636,16 @@ try {
} catch(\Exception $e) {
$transaction->rollBack();
throw $e;
} catch(\Throwable $e) {
$transaction->rollBack();
throw $e;
}
```
> Note: in the above code we have two catch-blocks for compatibility
> with PHP 5.x and PHP 7.x. `\Exception` implements the [`\Throwable` interface](http://php.net/manual/en/class.throwable.php)
> since PHP 7.0, so you can skip the part with `\Exception` if your app uses only PHP 7.0 and higher.
The second way is to list the DB operations that require transactional support in the [[yii\db\ActiveRecord::transactions()]]
method. For example,

20
docs/guide/db-dao.md

@ -328,18 +328,17 @@ The above code is equivalent to the following, which gives you more control abou
```php
$db = Yii::$app->db;
$transaction = $db->beginTransaction();
try {
$db->createCommand($sql1)->execute();
$db->createCommand($sql2)->execute();
// ... executing other SQL statements ...
$transaction->commit();
} catch(\Exception $e) {
$transaction->rollBack();
throw $e;
} catch(\Throwable $e) {
$transaction->rollBack();
throw $e;
}
```
@ -352,6 +351,10 @@ will be triggered and caught, the [[yii\db\Transaction::rollBack()|rollBack()]]
the changes made by the queries prior to that failed query in the transaction. `throw $e` will then re-throw the
exception as if we had not caught it, so the normal error handling process will take care of it.
> Note: in the above code we have two catch-blocks for compatibility
> with PHP 5.x and PHP 7.x. `\Exception` implements the [`\Throwable` interface](http://php.net/manual/en/class.throwable.php)
> since PHP 7.0, so you can skip the part with `\Exception` if your app uses only PHP 7.0 and higher.
### Specifying Isolation Levels <span id="specifying-isolation-levels"></span>
@ -424,12 +427,18 @@ try {
} catch (\Exception $e) {
$innerTransaction->rollBack();
throw $e;
} catch (\Throwable $e) {
$innerTransaction->rollBack();
throw $e;
}
$outerTransaction->commit();
} catch (\Exception $e) {
$outerTransaction->rollBack();
throw $e;
} catch (\Throwable $e) {
$outerTransaction->rollBack();
throw $e;
}
```
@ -573,6 +582,9 @@ try {
} catch(\Exception $e) {
$transaction->rollBack();
throw $e;
} catch(\Throwable $e) {
$transaction->rollBack();
throw $e;
}
```

BIN
docs/guide/images/start-gii-crud-preview.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 59 KiB

BIN
docs/guide/images/start-gii-crud.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 55 KiB

BIN
docs/guide/images/start-gii-model-preview.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 64 KiB

BIN
docs/guide/images/start-gii-model.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 50 KiB

3
docs/guide/input-validation.md

@ -331,7 +331,8 @@ the method/function is:
/**
* @param string $attribute the attribute currently being validated
* @param mixed $params the value of the "params" given in the rule
* @param \yii\validators\InlineValidator related InlineValidator instance
* @param \yii\validators\InlineValidator related InlineValidator instance.
* This parameter is available since version 2.0.11.
*/
function ($attribute, $params, $validator)
```

203
docs/guide/output-client-scripts.md

@ -1,70 +1,84 @@
Working with Client Scripts
===========================
> Note: This section is under development.
Modern web applications, additionally to static HTML pages that are
rendered and sent to the browser, contain JavaScript that is used
to modify the page in the browser by manipulating existing elements or
loading new content via AJAX.
This section describes methods provided by Yii for adding JavaScript and CSS to a website as well as dynamically adjusting these.
### Registering scripts
## Registering scripts <span id="register-scripts"></span>
With the [[yii\web\View]] object you can register scripts. There are two dedicated methods for it:
[[yii\web\View::registerJs()|registerJs()]] for inline scripts and
[[yii\web\View::registerJsFile()|registerJsFile()]] for external scripts.
Inline scripts are useful for configuration and dynamically generated code.
The method for adding these can be used as follows:
When working with the [[yii\web\View]] object you can dynamically register frontend scripts.
There are two dedicated methods for this:
- [[yii\web\View::registerJs()|registerJs()]] for inline scripts
- [[yii\web\View::registerJsFile()|registerJsFile()]] for external scripts
### Registering inline scripts <span id="inline-scripts"></span>
Inline scripts are useful for configuration, dynamically generated code and small snippets created by reusable frontend code contained in [widgets](structure-widgets.md).
The [[yii\web\View::registerJs()|registerJs()]] method for adding these can be used as follows:
```php
$this->registerJs("var options = ".json_encode($options).";", View::POS_END, 'my-options');
$this->registerJs(
"$('#myButton').on('click', function() { alert('Button clicked!'); });",
View::POS_READY,
'my-button-handler'
);
```
The first argument is the actual JS code we want to insert into the page. The second argument
determines where script should be inserted into the page. Possible values are:
The first argument is the actual JS code we want to insert into the page.
It will be wrapped into a `<script>` tag. The second argument
determines at which position the script should be inserted into the page. Possible values are:
- [[yii\web\View::POS_HEAD|View::POS_HEAD]] for head section.
- [[yii\web\View::POS_BEGIN|View::POS_BEGIN]] for right after opening `<body>`.
- [[yii\web\View::POS_END|View::POS_END]] for right before closing `</body>`.
- [[yii\web\View::POS_READY|View::POS_READY]] for executing code on document `ready` event. This will register [[yii\web\JqueryAsset|jQuery]] automatically.
- [[yii\web\View::POS_LOAD|View::POS_LOAD]] for executing code on document `load` event. This will register [[yii\web\JqueryAsset|jQuery]] automatically.
- [[yii\web\View::POS_READY|View::POS_READY]] for executing code on the [document `ready` event](http://learn.jquery.com/using-jquery-core/document-ready/).
This will automatically register [[yii\web\JqueryAsset|jQuery]] and wrap the code into the appropriate jQuery code. This is the default position.
- [[yii\web\View::POS_LOAD|View::POS_LOAD]] for executing code on the
[document `load` event](http://learn.jquery.com/using-jquery-core/document-ready/). Same as the above, this will also register [[yii\web\JqueryAsset|jQuery]] automatically.
The last argument is a unique script ID that is used to identify code block and replace existing one with the same ID
instead of adding a new one. If you don't provide it, the JS code itself will be used as the ID.
The last argument is a unique script ID that is used to identify the script code block and replace an existing one with the same ID
instead of adding a new one. If you don't provide it, the JS code itself will be used as the ID. It is used to avoid registration of the same code muliple times.
An external script can be added like the following:
```php
$this->registerJsFile('http://example.com/js/main.js', ['depends' => [\yii\web\JqueryAsset::className()]]);
```
### Registering script files <span id="script-files"></span>
The arguments for [[yii\web\View::registerJsFile()|registerJsFile()]] are similar to those for
[[yii\web\View::registerCssFile()|registerCssFile()]]. In the above example,
we register the `main.js` file with the dependency on `JqueryAsset`. This means the `main.js` file
will be added AFTER `jquery.js`. Without this dependency specification, the relative order between
`main.js` and `jquery.js` would be undefined.
Like for [[yii\web\View::registerCssFile()|registerCssFile()]], it is also highly recommended that you use
[asset bundles](structure-assets.md) to register external JS files rather than using [[yii\web\View::registerJsFile()|registerJsFile()]].
we register the `main.js` file with the dependency on the [[yii\web\JqueryAsset]]. It means that the `main.js` file
will be added AFTER `jquery.js`. Without such dependency specification, the relative order between
`main.js` and `jquery.js` would be undefined and the code would not work.
### Registering asset bundles
As was mentioned earlier it's preferred to use asset bundles instead of using CSS and JavaScript directly. You can get
details on how to define asset bundles in [asset manager](structure-assets.md) section of the guide. As for using already defined
asset bundle, it's very straightforward:
An external script can be added like the following:
```php
\frontend\assets\AppAsset::register($this);
$this->registerJsFile(
'@web/js/main.js',
['depends' => [\yii\web\JqueryAsset::className()]]
);
```
This will add a tag for the `/js/main.js` script located under the application [base URL](concept-aliases.md#predefined-aliases).
It is highly recommended to use [asset bundles](structure-assets.md) to register external JS files rather than [[yii\web\View::registerJsFile()|registerJsFile()]] because these allow better flexibility and more granular dependency configuration. Also using asset bundles allows you to combine and compress
multiple JS files, which is desirable for high traffic websites.
## Registering CSS <span id="register-css"></span>
### Registering CSS
Similar to Javascript, you can register CSS using
[[yii\web\View::registerCss()|registerCss()]] or
[[yii\web\View::registerCssFile()|registerCssFile()]].
The former registers a block of CSS code while the latter registers an external CSS file.
You can register CSS using [[yii\web\View::registerCss()|registerCss()]] or [[yii\web\View::registerCssFile()|registerCssFile()]].
The former registers a block of CSS code while the latter registers an external CSS file. For example,
### Registering inline CSS <span id="inline-css"></span>
```php
$this->registerCss("body { background: #f00; }");
```
The code above will result in adding the following to the head section of the page:
The code above will result in adding the following to the `<head>` section of the page:
```html
<style>
@ -73,26 +87,127 @@ body { background: #f00; }
```
If you want to specify additional properties of the style tag, pass an array of name-values to the second argument.
If you need to make sure there's only a single style tag use third argument as was mentioned in meta tags description.
The last argument is a unique ID that is used to identify the style block and make sure it is only added once in case the same style is registered from different places in the code.
### Registering CSS files <span id="css-files"></span>
A CSS file can be registered using the following:
```php
$this->registerCssFile("http://example.com/css/themes/black-and-white.css", [
'depends' => [BootstrapAsset::className()],
$this->registerCssFile("@web/css/themes/black-and-white.css", [
'depends' => [\yii\bootstrap\BootstrapAsset::className()],
'media' => 'print',
], 'css-print-theme');
```
The code above will add a link to CSS file to the head section of the page.
The above code will add a link to the `/css/themes/black-and-white.css` CSS file to the `<head>` section of the page.
* The first argument specifies the CSS file to be registered.
The `@web` in this example is an [alias for the applications base URL](concept-aliases.md#predefined-aliases).
* The second argument specifies the HTML attributes for the resulting `<link>` tag. The option `depends`
is specially handled. It specifies which asset bundles this CSS file depends on. In this case, the dependent
asset bundle is [[yii\bootstrap\BootstrapAsset|BootstrapAsset]]. This means the CSS file will be added
*after* the CSS files in [[yii\bootstrap\BootstrapAsset|BootstrapAsset]].
*after* the CSS files from [[yii\bootstrap\BootstrapAsset|BootstrapAsset]].
* The last argument specifies an ID identifying this CSS file. If it is not provided, the URL of the CSS file will be
used instead.
It is highly recommended that you use [asset bundles](structure-assets.md) to register external CSS files rather than
using [[yii\web\View::registerCssFile()|registerCssFile()]]. Using asset bundles allows you to combine and compress
It is highly recommended to use [asset bundles](structure-assets.md) to register external CSS files rather than
[[yii\web\View::registerCssFile()|registerCssFile()]]. Using asset bundles allows you to combine and compress
multiple CSS files, which is desirable for high traffic websites.
It also provides more flexibility as all asset dependencies of your application are configured in one place.
## Registering asset bundles <span id="asset-bundles"></span>
As was mentioned earlier it's recommended to use asset bundles instead of registering CSS and JavaScript files directly.
You can get details on how to define asset bundles in the
["Assets" section](structure-assets.md).
As for using already defined asset bundles, it's very straightforward:
```php
\frontend\assets\AppAsset::register($this);
```
In the above code, in the context of a view file, the `AppAsset` bundle is registered on the current view (represented by `$this`).
When registering asset bundles from within a widget, you would pass the
[[yii\base\Widget::$view|$view]] of the widget instead (`$this->view`).
## Generating Dynamic Javascript <span id="dynamic-js"></span>
In view files often the HTML code is not written out directly but generated
by some PHP code dependent on the variables of the view.
In order for the generated HTML to be manipulated with Javascript, the JS code has to contain dynamic parts too, for example the IDs of the jQuery selectors.
To insert PHP variables into JS code, their values have to be
escaped properly. Especially when the JS code is inserted into
HTML instead of residing in a dedicated JS file.
Yii provides the [[yii\helpers\Json::htmlEncode()|htmlEncode()]] method of the [[yii\helpers\Json|Json]] helper for this purpose. Its usage will be shown in the following examples.
### Registering a global JavaScript configuration <span id="js-configuration"></span>
In this example we use an array to pass global configuration parameters from
the PHP part of the application to the JS frontend code.
```php
$options = [
'appName' => Yii::$app->name,
'baseUrl' => Yii::$app->request->baseUrl,
'language' => Yii::$app->language,
// ...
];
$this->registerJs(
"var yiiOptions = ".\yii\helpers\Json::htmlEncode($options).";",
View::POS_HEAD,
'yiiOptions'
);
```
The above code will register a `<script>`-tag containing the JavaScript
variable definition, e.g.:
```javascript
var yiiOptions = {"appName":"My Yii Application","baseUrl":"/basic/web","language":"en"};
```
In your Javascript code you can now access these like `yiiOptions.baseUrl` or `yiiOptions.language`.
### Passing translated messages <span id="translated-messages"></span>
You may encounter a case where your JavaScript needs to print a message reacting to some event. In an application that works with multiple languages this string has to be translated to the current application language.
One way to achieve this is to use the
[message translation feature](tutorial-i18n.md#message-translation) provided by Yii and passing the result to the JavaScript code.
```php
$message = \yii\helpers\Json::htmlEncode(
\Yii::t('app', 'Button clicked!')
);
$this->registerJs(<<<JS
$('#myButton').on('click', function() { alert( $message ); });",
JS
);
```
The above example code uses PHP
[Heredoc syntax](http://php.net/manual/de/language.types.string.php#language.types.string.syntax.heredoc) for better readability. This also enables better syntax highlighting in most IDEs so it is the
preferred way of writing inline JavaScript, especially useful for code that is longer than a single line. The variable `$message` is created in PHP and
thanks to [[yii\helpers\Json::htmlEncode|Json::htmlEncode]] it contains the
string in valid JS syntax, which can be inserted into the JavaScript code to place the dynamic string in the function call to `alert()`.
> Note: When using Heredoc, be careful with variable naming in JS code
> as variables beginning with `$` may be interpreted as PHP variables which
> will be replaced by their content.
> The jQuery function in form of `$(` or `$.` is not interpreted
> as a PHP variable and can safely be used.
## The `yii.js` script <span id="yii.js"></span>
> Note: This section has not been written yet. It should contain explanation of the functionality provided by `yii.js`:
>
> - Yii JavaScript Modules
> - CSRF param handling
> - `data-confirm` handler
> - `data-method` handler
> - script filtering
> - redirect handling

35
docs/guide/rest-resources.md

@ -146,23 +146,41 @@ contains a single method [[yii\web\Linkable::getLinks()|getLinks()]] which shoul
Typically, you should return at least the `self` link representing the URL to the resource object itself. For example,
```php
use yii\db\ActiveRecord;
use yii\web\Link;
use yii\base\Model;
use yii\web\Link; // represents a link object as defined in JSON Hypermedia API Language.
use yii\web\Linkable;
use yii\helpers\Url;
class User extends ActiveRecord implements Linkable
class UserResource extends Model implements Linkable
{
public $id;
public $email;
//...
public function fields()
{
return ['id', 'email'];
}
public function extraFields()
{
return ['profile'];
}
public function getLinks()
{
return [
Link::REL_SELF => Url::to(['user/view', 'id' => $this->id], true),
'edit' => Url::to(['user/view', 'id' => $this->id], true),
'profile' => Url::to(['user/profile/view', 'id' => $this->id], true),
'index' => Url::to(['users'], true),
];
}
}
```
When a `User` object is returned in a response, it will contain a `_links` element representing the links related
When a `UserResource` object is returned in a response, it will contain a `_links` element representing the links related
to the user, for example,
```
@ -173,6 +191,15 @@ to the user, for example,
"_links" => {
"self": {
"href": "https://example.com/users/100"
},
"edit": {
"href": "https://example.com/users/100"
},
"profile": {
"href": "https://example.com/users/profile/100"
},
"index": {
"href": "https://example.com/users"
}
}
}

2
docs/guide/runtime-responses.md

@ -195,7 +195,7 @@ redirect the browser accordingly.
> Info: Yii comes with a `yii.js` JavaScript file which provides a set of commonly used JavaScript utilities,
including browser redirection based on the `X-Redirect` header. Therefore, if you are using this JavaScript file
(by registering the [[yii\web\YiiAsset]] asset bundle), you do not need to write anything to support AJAX redirection.
More information about `yii.js` can be found in the [Client Scripts Section](output-client-scripts.md#yii.js).
## Sending Files <span id="sending-files"></span>

1
docs/guide/structure-assets.md

@ -490,6 +490,7 @@ be referenced in your application or extension code.
- [[yii\web\YiiAsset]]: It mainly includes the `yii.js` file which implements a mechanism of organizing JavaScript code
in modules. It also provides special support for `data-method` and `data-confirm` attributes and other useful features.
More information about `yii.js` can be found in the [Client Scripts Section](output-client-scripts.md#yii.js).
- [[yii\web\JqueryAsset]]: It includes the `jquery.js` file from the jQuery Bower package.
- [[yii\bootstrap\BootstrapAsset]]: It includes the CSS file from the Twitter Bootstrap framework.
- [[yii\bootstrap\BootstrapPluginAsset]]: It includes the JavaScript file from the Twitter Bootstrap framework for

27
framework/CHANGELOG.md

@ -5,10 +5,12 @@ Yii Framework 2 Change Log
------------------------
- Bug #4113: Error page stacktrace was generating links to private methods which are not part of the API docs (samdark)
- Bug #7727: Fixed truncateHtml leaving extra tags (developeruz)
- Bug #7727: Fixed `yii\helpers\StringHelper::truncateHtml()` leaving extra tags (developeruz)
- Bug #9305: Fixed MSSQL `Schema::TYPE_TIMESTAMP` to be 'datetime' instead of 'timestamp', which is just an incremental number (nkovacs)
- Bug #9616: Fixed mysql\Schema::loadColumnSchema to set enumValues attribute correctly if enum definition contains commas (fphammerle)
- Bug #9796: Initialization of not existing `yii\grid\ActionColumn` default buttons (arogachev)
- Bug #11122: Fixed can not use `orderBy` with aggregate functions like `count` (Ni-san)
- Bug #11771: Fixed semantics of `yii\di\ServiceLocator::__isset()` to match the behavior of `__get()` which fixes inconsistent behavior on newer PHP versions (cebe)
- Bug #12213: Fixed `yii\db\ActiveRecord::unlinkAll()` to respect `onCondition()` of the relational query (silverfire)
- Bug #12681: Changed `data` column type from `text` to `blob` to handle null-byte (`\0`) in serialized RBAC rule properly (silverfire)
- Bug #12714: Fixed `yii\validation\EmailValidator` to prevent false-positives checks when property `checkDns` is set to `true` (silverfire)
@ -20,7 +22,7 @@ Yii Framework 2 Change Log
- Bug #12822: Fixed `yii\i18n\Formatter::asTimestamp()` to process timestamp with miliseconds correctly (h311ion)
- Bug #12824: Enabled usage of `yii\mutex\FileMutex` on Windows systems (davidsonalencar)
- Bug #12828: Fixed handling of nested arrays, objects in `\yii\grid\GridView::guessColumns` (githubjeka)
- Bug #12836: Fixed `yii\widgets\GridView::filterUrl` to not ignore `#` part of filter URL (cebe)
- Bug #12836: Fixed `yii\widgets\GridView::filterUrl` to not ignore `#` part of filter URL (cebe, arogachev)
- Bug #12856: Fixed `yii\web\XmlResponseFormatter` to use `true` and `false` to represent booleans (samdark)
- Bug #12879: Console progress bar was not working properly in Windows terminals (samdark, kids-return)
- Bug #12880: Fixed `yii\behaviors\AttributeTypecastBehavior` marks attributes with `null` value as 'dirty' (klimov-paul)
@ -30,10 +32,15 @@ Yii Framework 2 Change Log
- Bug #13071: Help option for commands was not working in modules (arogachev, haimanman)
- Bug #13089: Fixed `yii\console\controllers\AssetController::adjustCssUrl()` breaks URL reference specification (`url(#id)`) (vitalyzhakov)
- Bug #13105: Fixed `validate()` method in `yii.activeForm.js` to prevent unexpected form submit when `forceValidate` set to `true` (silverfire)
- Bug #13118: Fixed `handleAction()` function in `yii.js` to handle attribute `data-pjax=0` as disabled PJAX (silverfire)
- Bug #13118: Fixed `handleAction()` function in `yii.js` to handle attribute `data-pjax=0` as disabled PJAX (silverfire, arisk)
- Bug #13128: Fixed incorrect position of {pos} string in ColumnSchemaBuilder `__toString` (df2)
- Bug #13159: Fixed `destroy` method in `yii.captcha.js` which did not work as expected (arogachev)
- Bug #13198: Fixed order of checks in `yii\validators\IpValidator` that sometimes caused wrong error message (silverfire)
- Bug #13200: Creating URLs for routes specified in `yii\rest\UrlRule::$extraPatterns` did not work if no HTTP verb was specified (cebe)
- Bug #13229: Fix fetching table schema for `pgsql` when `PDO::ATTR_CASE` is set (klimov-paul)
- Bug #13231: Fixed `destroy` method in `yii.gridView.js` which did not work as expected (arogachev)
- Bug #13232: Event handlers were not detached with changed selector in `yii.gridView.js` (arogachev)
- Bug #13108: Fix execute command with negative integer parameter (pana1990, uaoleg)
- Enh #475: Added Bash and Zsh completion support for the `./yii` command (cebe, silverfire)
- Enh #6242: Access to validator in inline validation (arogachev)
- Enh #6373: Introduce `yii\db\Query::emulateExecution()` to force returning an empty result for a query (klimov-paul)
@ -57,10 +64,11 @@ Yii Framework 2 Change Log
- Enh #12619: Added catch `Throwable` in `yii\base\ErrorHandler::handleException()` (rob006)
- Enh #12659: Suggest alternatives when console command was not found (mdmunir, cebe)
- Enh #12726: `yii\base\Application::$version` converted to `yii\base\Module::$version` virtual property, allowing to specify version as a PHP callback (klimov-paul)
- Enh #12732: Added `is_dir()` validation to `yii\helpers\BaseFileHelper::findFiles()` method (zalatov, silverfire)
- Enh #12738: Added support for creating protocol-relative URLs in `UrlManager::createAbsoluteUrl()` and `Url` helper methods (rob006)
- Enh #12748: Added Migration tool automatic generation reference column for foreignKey (MKiselev)
- Enh #12748: Migration generator now tries to fetch reference column name for foreignKey from schema if it's not set explicitly (MKiselev)
- Enh #12750: `yii\widgets\ListView::itemOptions` can be a closure now (webdevsega, silverfire)
- Enh #12771: Skip \yii\rbac\PhpManager::checkAccessRecursive and \yii\rbac\DbManager::checkAccessRecursive if role assignments are empty (Ni-san)
- Enh #12790: Added `scrollToErrorOffset` option for `yii\widgets\ActiveForm` which adds ability to specify offset in pixels when scrolling to error (mg-code)
- Enh #12798: Changed `yii\cache\Dependency::getHasChanged()` (deprecated, to be removed in 2.1) to `yii\cache\Dependency::isChanged()` (dynasource)
- Enh #12807: Added console controller checks for `yii\console\controllers\HelpController` (schmunk42)
@ -68,12 +76,13 @@ Yii Framework 2 Change Log
- Enh #12854: Added `RangeNotSatisfiableHttpException` to cover HTTP error 416 file request exceptions (zalatov)
- Enh #12881: Added `removeValue` method to `yii\helpers\BaseArrayHelper` (nilsburg)
- Enh #12901: Added `getDefaultHelpHeader` method to the `yii\console\controllers\HelpController` class to be able to override default help header in a class heir (diezztsk)
- Enh #12988: Changed `textarea` method within the `yii\helpers\BaseHtml` class to allow users to control whether html entities found within `$value` will be double-encoded or not (cyphix333)
- Enh #12988: Changed `textarea` method within the `yii\helpers\BaseHtml` class to allow users to control whether HTML entities found within `$value` will be double-encoded or not (cyphix333)
- Enh #13020: Added `disabledListItemSubTagOptions` attribute for `yii\widgets\LinkPager` in order to customize the disabled list item sub tag element (nadar)
- Enh #13035: Use ArrayHelper::getValue() in SluggableBehavior::getValue() (thyseus)
- Enh #13036: Added shortcut methods `asJson()` and `asXml()` for returning JSON and XML data in web controller actions (cebe)
- Enh #13050: Added `yii\filters\HostControl` allowing protection against 'host header' attacks (klimov-paul, rob006)
- Enh #13074: Improved `yii\log\SyslogTarget` with `$options` to be able to change the default `openlog` options. (timbeks)
- Enh #13074: Improved `yii\log\SyslogTarget` with `$options` to be able to change the default `openlog` options (timbeks)
- Bug: #12969: Improved unique ID generation for `yii\widgets\Pjax` widgets (dynasource, samdark, rob006)
- Enh #13122: Optimized query for information about foreign keys in `yii\db\oci` (zlakomanoff)
- Enh #13202: Refactor validateAttribute method in UniqueValidator (developeruz)
- Enh: Added constants for specifying `yii\validators\CompareValidator::$type` (cebe)
@ -129,6 +138,7 @@ Yii Framework 2 Change Log
- Bug #12605: Make 'safe' validator work on write-only properties (arthibald, CeBe)
- Bug #12629: Fixed `yii\widgets\ActiveField::widget()` to call `adjustLabelFor()` for `InputWidget` descendants (coderlex)
- Bug #12649: Fixed consistency of `indexBy` handling for `yii\db\Query::column()` (silverfire)
- Bug #12713: Fixed `yii\caching\FileDependency` to clear stat cache before reading filemtime (SG5)
- Enh #384: Added ability to run migration from several locations via `yii\console\controllers\BaseMigrateController::$migrationNamespaces` (klimov-paul)
- Enh #6996: Added `yii\web\MultipartFormDataParser`, which allows proper processing of 'multipart/form-data' encoded non POST requests (klimov-paul)
- Enh #8719: Add support for HTML5 attributes on submitbutton (formaction/formmethod...) for ActiveForm (VirtualRJ)
@ -163,11 +173,13 @@ Yii Framework 2 Change Log
- Enh #12440: Added `yii\base\Event::offAll()` method allowing clear all registered class-level event handlers (klimov-paul)
- Enh #12499: When AJAX validation in enabled, `yii.activeForm.js` will run it forcefully on form submit to display all possible errors (silverfire)
- Enh #12580: Make `yii.js` comply with strict and non-strict javascript mode to allow concatenation with external code (mikehaertl)
- Enh #12612: Query conditions added with `yii\db\Query::andWhere()` now get appended to the existing conditions if they were already being joined with the `and` operator (brandonkelly)
- Enh #12664: Added support for wildcards for `optional` at `yii\filters\auth\AuthMethod` (mg-code)
- Enh #12744: Added `afterInit` event to `yii.activeForm.js` (werew01f)
- Enh #12710: Added `beforeItem` and `afterItem` to `yii\widgets\ListView` (mdmunir, silverfire)
- Enh #12727: Enhanced `yii\widgets\Menu` to allow item option `active` be a Closure (voskobovich, silverfire)
- Enh: Method `yii\console\controllers\AssetController::getAssetManager()` automatically enables `yii\web\AssetManager::forceCopy` in case it is not explicitly specified (pana1990, klimov-paul)
2.0.9 July 11, 2016
-------------------
@ -228,6 +240,7 @@ Yii Framework 2 Change Log
- Enh #11857: `yii\filters\AccessRule::$verbs` can now be configured in upper and lowercase (DrDeath72, samdark)
- Chg #11364: Updated jQuery dependency to include versions `1.12.*` (cebe)
- Chg #11683: Fixed fixture command to work with short syntax. `yii fixture "*, -User"` should be used instead of `yii fixture "*" -User` (Faryshta, samdark)
- Chg #11906: Updated `yii\widgets\MaskedInput` inputmask dependency to `~3.3.3` (samdark)
2.0.8 April 28, 2016

10
framework/UPGRADE.md

@ -60,6 +60,16 @@ Upgrade from Yii 2.0.10
* `yii\validators\FileValidator::getClientOptions()` and `yii\validators\ImageValidator::getClientOptions()` are now public.
If you extend from these classes and override these methods, you must make them public as well.
* `yii\widgets\MaskedInput` inputmask dependency was updated to `~3.3.3`.
[See its changelog for details](https://github.com/RobinHerbots/Inputmask/blob/3.x/CHANGELOG.md).
* PJAX: Auto generated IDs of the Pjax widget have been changed to use their own prefix to avoid conflicts.
Auto generated IDs are now prefixed with `p` instead of `w`. This is defined by the `$autoIdPrefix`
property of `yii\widgets\Pjax`. If you have any PHP or Javascript code that depends on autogenerated IDs
you should update these to match this new value. It is not a good idea to rely on auto generated values anyway, so
you better fix these cases by specifying an explicit ID.
Upgrade from Yii 2.0.9
----------------------

80
framework/assets/yii.gridView.js

@ -16,7 +16,7 @@
} else if (typeof method === 'object' || !method) {
return methods.init.apply(this, arguments);
} else {
$.error('Method ' + method + ' does not exist on jQuery.yiiGridView');
$.error('Method ' + method + ' does not exist in jQuery.yiiGridView');
return false;
}
};
@ -50,6 +50,32 @@
afterFilter: 'afterFilter'
};
/**
* Used for storing active event handlers and removing them later.
* The structure of single event handler is:
*
* {
* gridViewId: {
* type: {
* event: '...',
* selector: '...'
* }
* }
* }
*
* Used types:
*
* - filter, used for filtering grid with elements found by filterSelector
* - checkRow, used for checking single row
* - checkAllRows, used for checking all rows with according "Check all" checkbox
*
* event is the name of event, for example: 'change.yiiGridView'
* selector is a jQuery selector for finding elements
*
* @type {{}}
*/
var gridEventHandlers = {};
var methods = {
init: function (options) {
return this.each(function () {
@ -62,9 +88,9 @@
gridData[id] = $.extend(gridData[id], {settings: settings});
var filterEvents = 'change.yiiGridView keydown.yiiGridView';
var enterPressed = false;
$(document).off('change.yiiGridView keydown.yiiGridView', settings.filterSelector)
.on('change.yiiGridView keydown.yiiGridView', settings.filterSelector, function (event) {
initEventHandler($e, 'filter', filterEvents, settings.filterSelector, function (event) {
if (event.type === 'keydown') {
if (event.keyCode !== 13) {
return; // only react to enter key
@ -87,7 +113,7 @@
},
applyFilter: function () {
var $grid = $(this), event;
var $grid = $(this);
var settings = gridData[$grid.attr('id')].settings;
var data = {};
$.each($(settings.filterSelector).serializeArray(), function () {
@ -119,8 +145,8 @@
var pos = settings.filterUrl.indexOf('?');
var url = pos < 0 ? settings.filterUrl : settings.filterUrl.substring(0, pos);
var hashPos = settings.filterUrl.indexOf('#');
if (hashPos >= 0) {
url += settings.filterUrl.substring(pos);
if (pos >= 0 && hashPos >= 0) {
url += settings.filterUrl.substring(hashPos);
}
$grid.find('form.gridview-filter-form').remove();
@ -137,7 +163,7 @@
});
});
event = $.Event(gridEvents.beforeFilter);
var event = $.Event(gridEvents.beforeFilter);
$grid.trigger(event);
if (event.result === false) {
return;
@ -161,10 +187,10 @@
var checkAll = "#" + id + " input[name='" + options.checkAll + "']";
var inputs = options['class'] ? "input." + options['class'] : "input[name='" + options.name + "']";
var inputsEnabled = "#" + id + " " + inputs + ":enabled";
$(document).off('click.yiiGridView', checkAll).on('click.yiiGridView', checkAll, function () {
initEventHandler($grid, 'checkAllRows', 'click.yiiGridView', checkAll, function () {
$grid.find(inputs + ":enabled").prop('checked', this.checked);
});
$(document).off('click.yiiGridView', inputsEnabled).on('click.yiiGridView', inputsEnabled, function () {
initEventHandler($grid, 'checkRow', 'click.yiiGridView', inputsEnabled, function () {
var all = $grid.find(inputs).length == $grid.find(inputs + ":checked").length;
$grid.find("input[name='" + options.checkAll + "']").prop('checked', all);
});
@ -183,10 +209,17 @@
},
destroy: function () {
return this.each(function () {
$(window).unbind('.yiiGridView');
$(this).removeData('yiiGridView');
var events = ['.yiiGridView', gridEvents.beforeFilter, gridEvents.afterFilter].join(' ');
this.off(events);
var id = $(this).attr('id');
$.each(gridEventHandlers[id], function (type, data) {
$(document).off(data.event, data.selector);
});
delete gridData[id];
return this;
},
data: function () {
@ -194,4 +227,27 @@
return gridData[id];
}
};
/**
* Used for attaching event handler and prevent of duplicating them. With each call previously attached handler of
* the same type is removed even selector was changed.
* @param {jQuery} $gridView According jQuery grid view element
* @param {string} type Type of the event which acts like a key
* @param {string} event Event name, for example 'change.yiiGridView'
* @param {string} selector jQuery selector
* @param {function} callback The actual function to be executed with this event
*/
function initEventHandler($gridView, type, event, selector, callback) {
var id = $gridView.attr('id');
var prevHandler = gridEventHandlers[id];
if (prevHandler !== undefined && prevHandler[type] !== undefined) {
var data = prevHandler[type];
$(document).off(data.event, data.selector);
}
if (prevHandler === undefined) {
gridEventHandlers[id] = {};
}
$(document).on(event, selector, callback);
gridEventHandlers[id][type] = {event: event, selector: selector};
}
})(window.jQuery);

9
framework/assets/yii.js

@ -154,6 +154,7 @@ window.yii = (function ($) {
action = $e.attr('href'),
params = $e.data('params'),
pjax = $e.data('pjax') || 0,
usePjax = pjax !== 0 && $.support.pjax,
pjaxPushState = !!$e.data('pjax-push-state'),
pjaxReplaceState = !!$e.data('pjax-replace-state'),
pjaxTimeout = $e.data('pjax-timeout'),
@ -164,7 +165,7 @@ window.yii = (function ($) {
pjaxContainer,
pjaxOptions = {};
if (pjax !== 0 && $.support.pjax) {
if (usePjax) {
if ($e.data('pjax-container')) {
pjaxContainer = $e.data('pjax-container');
} else {
@ -190,13 +191,13 @@ window.yii = (function ($) {
if (method === undefined) {
if (action && action != '#') {
if (pjax !== 0 && $.support.pjax) {
if (usePjax) {
$.pjax.click(event, pjaxOptions);
} else {
window.location = action;
}
} else if ($e.is(':submit') && $form.length) {
if (pjax !== 0 && $.support.pjax) {
if (usePjax) {
$form.on('submit',function(e){
$.pjax.submit(e, pjaxOptions);
})
@ -249,7 +250,7 @@ window.yii = (function ($) {
oldAction = $form.attr('action');
$form.attr('action', action);
}
if (pjax !== 0 && $.support.pjax) {
if (usePjax) {
$form.on('submit',function(e){
$.pjax.submit(e, pjaxOptions);
})

2
framework/caching/ArrayCache.php

@ -15,6 +15,8 @@ namespace yii\caching;
* Unlike the [[Cache]], ArrayCache allows the expire parameter of [[set]], [[add]], [[multiSet]] and [[multiAdd]] to
* be a floating point number, so you may specify the time in milliseconds (e.g. 0.1 will be 100 milliseconds).
*
* For enhanced performance of ArrayCache, you can disable serialization of the stored data by setting [[$serializer]] to `false`.
*
* For more details and usage information on Cache, see the [guide article on caching](guide:caching-overview).
*
* @author Carsten Brandt <mail@cebe.cc>

4
framework/caching/FileDependency.php

@ -43,6 +43,8 @@ class FileDependency extends Dependency
throw new InvalidConfigException('FileDependency::fileName must be set');
}
return @filemtime(Yii::getAlias($this->fileName));
$fileName = Yii::getAlias($this->fileName);
clearstatcache(false, $fileName);
return @filemtime($fileName);
}
}

2
framework/composer.json

@ -65,7 +65,7 @@
"ezyang/htmlpurifier": "~4.6",
"cebe/markdown": "~1.0.0 | ~1.1.0",
"bower-asset/jquery": "2.2.*@stable | 2.1.*@stable | 1.11.*@stable | 1.12.*@stable",
"bower-asset/jquery.inputmask": "~3.2.2",
"bower-asset/jquery.inputmask": "~3.2.2 | ~3.3.3",
"bower-asset/punycode": "1.3.*",
"bower-asset/yii2-pjax": "~2.0.1"
},

4
framework/console/Request.php

@ -73,7 +73,11 @@ class Request extends \yii\base\Request
}
} elseif (preg_match('/^-(\w+)(?:=(.*))?$/', $param, $matches)) {
$name = $matches[1];
if (is_numeric($name)) {
$params[] = $param;
} else {
$params['_aliases'][$name] = isset($matches[2]) ? $matches[2] : true;
}
} else {
$params[] = $param;
}

1
framework/data/ActiveDataProvider.php

@ -7,7 +7,6 @@
namespace yii\data;
use Yii;
use yii\db\ActiveQueryInterface;
use yii\base\InvalidConfigException;
use yii\base\Model;

9
framework/db/ActiveRecord.php

@ -439,6 +439,9 @@ class ActiveRecord extends BaseActiveRecord
} catch (\Exception $e) {
$transaction->rollBack();
throw $e;
} catch (\Throwable $e) {
$transaction->rollBack();
throw $e;
}
}
@ -545,6 +548,9 @@ class ActiveRecord extends BaseActiveRecord
} catch (\Exception $e) {
$transaction->rollBack();
throw $e;
} catch (\Throwable $e) {
$transaction->rollBack();
throw $e;
}
}
@ -585,6 +591,9 @@ class ActiveRecord extends BaseActiveRecord
} catch (\Exception $e) {
$transaction->rollBack();
throw $e;
} catch (\Throwable $e) {
$transaction->rollBack();
throw $e;
}
}

17
framework/db/Connection.php

@ -420,7 +420,7 @@ class Connection extends Component
* Use 0 to indicate that the cached data will never expire.
* @param \yii\caching\Dependency $dependency the cache dependency associated with the cached query results.
* @return mixed the return result of the callable
* @throws \Exception if there is any exception during query
* @throws \Exception|\Throwable if there is any exception during query
* @see enableQueryCache
* @see queryCache
* @see noCache()
@ -435,6 +435,9 @@ class Connection extends Component
} catch (\Exception $e) {
array_pop($this->_queryCacheInfo);
throw $e;
} catch (\Throwable $e) {
array_pop($this->_queryCacheInfo);
throw $e;
}
}
@ -457,7 +460,7 @@ class Connection extends Component
* @param callable $callable a PHP callable that contains DB queries which should not use query cache.
* The signature of the callable is `function (Connection $db)`.
* @return mixed the return result of the callable
* @throws \Exception if there is any exception during query
* @throws \Exception|\Throwable if there is any exception during query
* @see enableQueryCache
* @see queryCache
* @see cache()
@ -472,6 +475,9 @@ class Connection extends Component
} catch (\Exception $e) {
array_pop($this->_queryCacheInfo);
throw $e;
} catch (\Throwable $e) {
array_pop($this->_queryCacheInfo);
throw $e;
}
}
@ -671,7 +677,7 @@ class Connection extends Component
* @param callable $callback a valid PHP callback that performs the job. Accepts connection instance as parameter.
* @param string|null $isolationLevel The isolation level to use for this transaction.
* See [[Transaction::begin()]] for details.
* @throws \Exception
* @throws \Exception|\Throwable if there is any exception during query. In this case the transaction will be rolled back.
* @return mixed result of callback function
*/
public function transaction(callable $callback, $isolationLevel = null)
@ -689,6 +695,11 @@ class Connection extends Component
$transaction->rollBack();
}
throw $e;
} catch (\Throwable $e) {
if ($transaction->isActive && $transaction->level === $level) {
$transaction->rollBack();
}
throw $e;
}
return $result;

27
framework/db/Migration.php

@ -95,15 +95,16 @@ class Migration extends Component implements MigrationInterface
try {
if ($this->safeUp() === false) {
$transaction->rollBack();
return false;
}
$transaction->commit();
} catch (\Exception $e) {
echo 'Exception: ' . $e->getMessage() . ' (' . $e->getFile() . ':' . $e->getLine() . ")\n";
echo $e->getTraceAsString() . "\n";
$this->printException($e);
$transaction->rollBack();
return false;
} catch (\Throwable $e) {
$this->printException($e);
$transaction->rollBack();
return false;
}
@ -123,15 +124,16 @@ class Migration extends Component implements MigrationInterface
try {
if ($this->safeDown() === false) {
$transaction->rollBack();
return false;
}
$transaction->commit();
} catch (\Exception $e) {
echo 'Exception: ' . $e->getMessage() . ' (' . $e->getFile() . ':' . $e->getLine() . ")\n";
echo $e->getTraceAsString() . "\n";
$this->printException($e);
$transaction->rollBack();
return false;
} catch (\Throwable $e) {
$this->printException($e);
$transaction->rollBack();
return false;
}
@ -139,6 +141,15 @@ class Migration extends Component implements MigrationInterface
}
/**
* @param \Throwable|\Exception $e
*/
private function printException($e)
{
echo 'Exception: ' . $e->getMessage() . ' (' . $e->getFile() . ':' . $e->getLine() . ")\n";
echo $e->getTraceAsString() . "\n";
}
/**
* This method contains the logic to be executed when applying this migration.
* This method differs from [[up()]] in that the DB logic implemented here will
* be enclosed within a DB transaction.

10
framework/db/Query.php

@ -421,7 +421,13 @@ class Query extends Component implements QueryInterface
$this->limit = $limit;
$this->offset = $offset;
if (empty($this->groupBy) && empty($this->having) && empty($this->union) && !$this->distinct) {
if (
!$this->distinct
&& empty($this->groupBy)
&& empty($this->having)
&& empty($this->union)
&& empty($this->orderBy)
) {
return $command->queryScalar();
} else {
return (new Query)->select([$selectExpression])
@ -585,6 +591,8 @@ class Query extends Component implements QueryInterface
{
if ($this->where === null) {
$this->where = $condition;
} elseif (is_array($this->where) && isset($this->where[0]) && strcasecmp($this->where[0], 'and') === 0) {
$this->where[] = $condition;
} else {
$this->where = ['and', $this->where, $condition];
}

7
framework/db/Transaction.php

@ -28,9 +28,16 @@ use yii\base\InvalidConfigException;
* } catch (\Exception $e) {
* $transaction->rollBack();
* throw $e;
* } catch (\Throwable $e) {
* $transaction->rollBack();
* throw $e;
* }
* ```
*
* > Note: in the above code we have two catch-blocks for compatibility
* > with PHP 5.x and PHP 7.x. `\Exception` implements the [`\Throwable` interface](http://php.net/manual/en/class.throwable.php)
* > since PHP 7.0, so you can skip the part with `\Exception` if your app uses only PHP 7.0 and higher.
*
* @property bool $isActive Whether this transaction is active. Only an active transaction can [[commit()]]
* or [[rollBack()]]. This property is read-only.
* @property string $isolationLevel The transaction isolation level to use for this transaction. This can be

3
framework/db/pgsql/Schema.php

@ -454,6 +454,9 @@ SQL;
return false;
}
foreach ($columns as $column) {
if ($this->db->slavePdo->getAttribute(\PDO::ATTR_CASE) === \PDO::CASE_UPPER) {
$column = array_change_key_case($column, CASE_LOWER);
}
$column = $this->loadColumnSchema($column);
$table->columns[$column->name] = $column;
if ($column->isPrimaryKey) {

2
framework/di/ServiceLocator.php

@ -84,7 +84,7 @@ class ServiceLocator extends Component
*/
public function __isset($name)
{
if ($this->has($name, true)) {
if ($this->has($name)) {
return true;
} else {
return parent::__isset($name);

2
framework/helpers/BaseFileHelper.php

@ -411,7 +411,7 @@ class BaseFileHelper
if (static::filterPath($path, $options)) {
if (is_file($path)) {
$list[] = $path;
} elseif (!isset($options['recursive']) || $options['recursive']) {
} elseif (is_dir($path) && (!isset($options['recursive']) || $options['recursive'])) {
$list = array_merge($list, static::findFiles($path, $options));
}
}

9
framework/helpers/BaseJson.php

@ -40,9 +40,14 @@ class BaseJson
/**
* Encodes the given value into a JSON string.
*
* The method enhances `json_encode()` by supporting JavaScript expressions.
* In particular, the method will not encode a JavaScript expression that is
* represented in terms of a [[JsExpression]] object.
*
* Note that data encoded as JSON must be UTF-8 encoded according to the JSON specification.
* You must ensure strings passed to this method have proper encoding before passing them.
*
* @param mixed $value the data to be encoded.
* @param int $options the encoding options. For more details please refer to
* <http://www.php.net/manual/en/function.json-encode.php>. Default is `JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE`.
@ -65,10 +70,14 @@ class BaseJson
/**
* Encodes the given value into a JSON string HTML-escaping entities so it is safe to be embedded in HTML code.
*
* The method enhances `json_encode()` by supporting JavaScript expressions.
* In particular, the method will not encode a JavaScript expression that is
* represented in terms of a [[JsExpression]] object.
*
* Note that data encoded as JSON must be UTF-8 encoded according to the JSON specification.
* You must ensure strings passed to this method have proper encoding before passing them.
*
* @param mixed $value the data to be encoded
* @return string the encoding result
* @since 2.0.4

21
framework/i18n/Formatter.php

@ -920,8 +920,13 @@ class Formatter extends Component
* value is rounded automatically to the defined decimal digits.
*
* @param mixed $value the value to be formatted.
* @param int $decimals the number of digits after the decimal point. If not given the number of digits is determined from the
* [[locale]] and if the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available defaults to `2`.
* @param int $decimals the number of digits after the decimal point.
* If not given, the number of digits depends in the input value and is determined based on
* `NumberFormatter::MIN_FRACTION_DIGITS` and `NumberFormatter::MAX_FRACTION_DIGITS`, which can be configured
* using [[$numberFormatterOptions]].
* If the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available, the default value is `2`.
* If you want consistent behavior between environments where intl is available and not, you should explicitly
* specify a value here.
* @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
* @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
* @return string the formatted result.
@ -956,6 +961,12 @@ class Formatter extends Component
*
* @param mixed $value the value to be formatted. It must be a factor e.g. `0.75` will result in `75%`.
* @param int $decimals the number of digits after the decimal point.
* If not given, the number of digits depends in the input value and is determined based on
* `NumberFormatter::MIN_FRACTION_DIGITS` and `NumberFormatter::MAX_FRACTION_DIGITS`, which can be configured
* using [[$numberFormatterOptions]].
* If the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available, the default value is `0`.
* If you want consistent behavior between environments where intl is available and not, you should explicitly
* specify a value here.
* @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
* @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
* @return string the formatted result.
@ -988,6 +999,12 @@ class Formatter extends Component
*
* @param mixed $value the value to be formatted.
* @param int $decimals the number of digits after the decimal point.
* If not given, the number of digits depends in the input value and is determined based on
* `NumberFormatter::MIN_FRACTION_DIGITS` and `NumberFormatter::MAX_FRACTION_DIGITS`, which can be configured
* using [[$numberFormatterOptions]].
* If the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available, the default value depends on your PHP configuration.
* If you want consistent behavior between environments where intl is available and not, you should explicitly
* specify a value here.
* @param array $options optional configuration for the number formatter. This parameter will be merged with [[numberFormatterOptions]].
* @param array $textOptions optional configuration for the number formatter. This parameter will be merged with [[numberFormatterTextOptions]].
* @return string the formatted result.

23
framework/messages/fa/yii.php

@ -5,7 +5,7 @@
*
* Message translations.
*
* This file is automatically generated by 'yii message' command.
* This file is automatically generated by 'yii message/extract' command.
* It contains the localizable messages extracted from source code.
* You may modify this file by translating the extracted messages.
*
@ -20,10 +20,11 @@
* NOTE: this file must be saved in UTF-8 encoding.
*/
return [
'The combination {values} of {attributes} has already been taken.' => 'مقدار {values} از {attributes} قبلاً گرفته شده است.',
'Unknown alias: -{name}' => 'نام مستعار ناشناخته: -{name}',
'(not set)' => '(تنظیم نشده)',
'An internal server error occurred.' => 'خطای داخلی سرور رخ داده است.',
'Are you sure you want to delete this item?' => 'آیا اطمینان به حذف این مورد دارید؟',
'Delete' => 'حذف',
'Error' => 'خطا',
'File upload failed.' => 'آپلود فایل ناموفق بود.',
'Home' => 'صفحهاصلی',
@ -38,7 +39,6 @@ return [
'Page not found.' => 'صفحهای یافت نشد.',
'Please fix the following errors:' => 'لطفاً خطاهای زیر را رفع نمائید:',
'Please upload a file.' => 'لطفاً یک فایل آپلود کنید.',
'Powered by {yii}' => 'طراحی شده توسط {yii}',
'Showing <b>{begin, number}-{end, number}</b> of <b>{totalCount, number}</b> {totalCount, plural, one{item} other{items}}.' => 'نمایش <b>{begin, number} تا {end, number}</b> مورد از کل <b>{totalCount, number}</b> مورد.',
'The file "{file}" is not an image.' => 'فایل "{file}" یک تصویر نیست.',
'The file "{file}" is too big. Its size cannot exceed {formattedLimit}.' => 'حجم فایل "{file}" بسیار بیشتر می باشد. حجم آن نمی تواند از {formattedLimit} بیشتر باشد.',
@ -53,10 +53,7 @@ return [
'Total <b>{count, number}</b> {count, plural, one{item} other{items}}.' => 'مجموع <b>{count, number}</b> مورد.',
'Unable to verify your data submission.' => 'قادر به تأیید اطلاعات ارسالی شما نمیباشد.',
'Unknown option: --{name}' => 'گزینه ناشناخته: --{name}',
'Update' => 'بروزرسانی',
'View' => 'نما',
'Yes' => 'بله',
'Yii Framework' => 'فریم ورک یی',
'You are not allowed to perform this action.' => 'شما برای انجام این عملیات، دسترسی ندارید.',
'You can upload at most {limit, number} {limit, plural, one{file} other{files}}.' => 'شما حداکثر {limit, number} فایل را میتوانید آپلود کنید.',
'in {delta, plural, =1{a day} other{# days}}' => '{delta} روز دیگر',
@ -73,25 +70,25 @@ return [
'{attribute} is invalid.' => '{attribute} معتبر نیست.',
'{attribute} is not a valid URL.' => '{attribute} یک URL معتبر نیست.',
'{attribute} is not a valid email address.' => '{attribute} یک آدرس ایمیل معتبر نیست.',
'{attribute} is not in the allowed range.' => '{attribute} در محدوده مجاز نمی باشد.',
'{attribute} is not in the allowed range.' => '{attribute} در محدوده مجاز نمیباشد.',
'{attribute} must be "{requiredValue}".' => '{attribute} باید "{requiredValue}" باشد.',
'{attribute} must be a number.' => '{attribute} باید یک عدد باشد.',
'{attribute} must be a string.' => '{attribute} باید یک رشته باشد.',
'{attribute} must be a valid IP address.' => '{attribute} باید IP صحیح باشد.',
'{attribute} must be an IP address with specified subnet.' => '{attribute} باید یک IP آدرسی با زیرشبکه بخصوص باشد.',
'{attribute} must be a valid IP address.' => '{attribute} باید یک آدرس IP معتبر باشد.',
'{attribute} must be an IP address with specified subnet.' => '{attribute} باید یک IP آدرس با زیرشبکه بخصوص باشد.',
'{attribute} must be an integer.' => '{attribute} باید یک عدد صحیح باشد.',
'{attribute} must be either "{true}" or "{false}".' => '{attribute} باید "{true}" و یا "{false}" باشد.',
'{attribute} must be equal to "{compareValueOrAttribute}".' => '{attribute} باید با "{compareValueOrAttribute}" برابر باشد.',
'{attribute} must be greater than "{compareValueOrAttribute}".' => '{attribute} باید بزرگتر از "{compareValueOrAttribute}" باشد.',
'{attribute} must be greater than or equal to "{compareValueOrAttribute}".' => '{attribute} باید بزرکتر یا برابر با "{compareValueOrAttribute}" باشد.',
'{attribute} must be greater than or equal to "{compareValueOrAttribute}".' => '{attribute} باید بزرگتر یا برابر با "{compareValueOrAttribute}" باشد.',
'{attribute} must be less than "{compareValueOrAttribute}".' => '{attribute} باید کمتر از "{compareValueOrAttribute}" باشد.',
'{attribute} must be less than or equal to "{compareValueOrAttribute}".' => '{attribute} باید کمتر یا برابر با "{compareValueOrAttribute}" باشد.',
'{attribute} must be no greater than {max}.' => '{attribute} نباید بیشتر از "{max}" باشد.',
'{attribute} must be no less than {min}.' => '{attribute} نباید کمتر از "{min}" باشد.',
'{attribute} must not be a subnet.' => '{attribute} نباید یک زیرشبکه باشد.',
'{attribute} must not be an IPv4 address.' => '{attribute} باید آدرس IPv4 نباشد.',
'{attribute} must not be an IPv6 address.' => '{attribute} باید آدرس IPv6 نباشد.',
'{attribute} must not be equal to "{compareValueOrAttribute}".' => '{attribute} باید مانند "{compareValueOrAttribute}" تکرار نشود.',
'{attribute} must not be an IPv4 address.' => '{attribute} نباید آدرس IPv4 باشد.',
'{attribute} must not be an IPv6 address.' => '{attribute} نباید آدرس IPv6 باشد.',
'{attribute} must not be equal to "{compareValueOrAttribute}".' => '{attribute} نباید برابر با "{compareValueOrAttribute}" باشد.',
'{attribute} should contain at least {min, number} {min, plural, one{character} other{characters}}.' => '{attribute} حداقل باید شامل {min, number} کارکتر باشد.',
'{attribute} should contain at most {max, number} {max, plural, one{character} other{characters}}.' => '{attribute} حداکثر باید شامل {max, number} کارکتر باشد.',
'{attribute} should contain {length, number} {length, plural, one{character} other{characters}}.' => '{attribute} باید شامل {length, number} کارکتر باشد.',

12
framework/rbac/BaseManager.php

@ -222,4 +222,16 @@ abstract class BaseManager extends Component implements ManagerInterface
throw new InvalidConfigException("Rule not found: {$item->ruleName}");
}
}
/**
* Checks whether array of $assignments is empty and [[defaultRoles]] property is empty as well
*
* @param Assignment[] $assignments array of user's assignments
* @return bool whether array of $assignments is empty and [[defaultRoles]] property is empty as well
* @since 2.0.11
*/
protected function hasNoAssignments(array $assignments)
{
return empty($assignments) && empty($this->defaultRoles);
}
}

5
framework/rbac/DbManager.php

@ -121,6 +121,11 @@ class DbManager extends BaseManager
public function checkAccess($userId, $permissionName, $params = [])
{
$assignments = $this->getAssignments($userId);
if ($this->hasNoAssignments($assignments)) {
return false;
}
$this->loadFromCache();
if ($this->items !== null) {
return $this->checkAccessFromCache($userId, $permissionName, $params, $assignments);

5
framework/rbac/PhpManager.php

@ -99,6 +99,11 @@ class PhpManager extends BaseManager
public function checkAccess($userId, $permissionName, $params = [])
{
$assignments = $this->getAssignments($userId);
if ($this->hasNoAssignments($assignments)) {
return false;
}
return $this->checkAccessRecursive($userId, $permissionName, $params, $assignments);
}

2
framework/rest/UrlRule.php

@ -203,7 +203,7 @@ class UrlRule extends CompositeUrlRule
$config['verb'] = $verbs;
$config['pattern'] = rtrim($prefix . '/' . strtr($pattern, $this->tokens), '/');
$config['route'] = $action;
if (!in_array('GET', $verbs)) {
if (!empty($verbs) && !in_array('GET', $verbs)) {
$config['mode'] = \yii\web\UrlRule::PARSING_ONLY;
}
$config['suffix'] = $this->suffix;

2
framework/validators/InlineValidator.php

@ -35,7 +35,7 @@ class InlineValidator extends Validator
*
* - `$attribute` is the name of the attribute to be validated;
* - `$params` contains the value of [[params]] that you specify when declaring the inline validation rule;
* - `$validator` is a reference to related [[InlineValidator]] object.
* - `$validator` is a reference to related [[InlineValidator]] object. This parameter is available since version 2.0.11.
*/
public $method;
/**

83
framework/widgets/ListView.php

@ -7,7 +7,6 @@
namespace yii\widgets;
use Yii;
use Closure;
use yii\helpers\ArrayHelper;
use yii\helpers\Html;
@ -75,7 +74,36 @@ class ListView extends BaseListView
* @see \yii\helpers\Html::renderTagAttributes() for details on how attributes are being rendered.
*/
public $options = ['class' => 'list-view'];
/**
* @var Closure an anonymous function that is called once BEFORE rendering each data model.
* It should have the following signature:
*
* ```php
* function ($model, $key, $index, $widget)
* ```
*
* - `$model`: the current data model being rendered
* - `$key`: the key value associated with the current data model
* - `$index`: the zero-based index of the data model in the model array returned by [[dataProvider]]
* - `$widget`: the ListView object
*
* The return result of the function will be rendered directly.
* Note: If the function returns `null`, nothing will be rendered before the item.
* @see renderBeforeItem
* @since 2.0.11
*/
public $beforeItem;
/**
* @var Closure an anonymous function that is called once AFTER rendering each data model.
*
* It should have the same signature as [[beforeItem]].
*
* The return result of the function will be rendered directly.
* Note: If the function returns `null`, nothing will be rendered after the item.
* @see renderAfterItem
* @since 2.0.11
*/
public $afterItem;
/**
* Renders all data models.
@ -87,13 +115,62 @@ class ListView extends BaseListView
$keys = $this->dataProvider->getKeys();
$rows = [];
foreach (array_values($models) as $index => $model) {
$rows[] = $this->renderItem($model, $keys[$index], $index);
$key = $keys[$index];
if (($before = $this->renderBeforeItem($model, $key, $index)) !== null) {
$rows[] = $before;
}
$rows[] = $this->renderItem($model, $key, $index);
if (($after = $this->renderAfterItem($model, $key, $index)) !== null) {
$rows[] = $after;
}
}
return implode($this->separator, $rows);
}
/**
* Calls [[beforeItem]] closure, returns execution result.
* If [[beforeItem]] is not a closure, `null` will be returned.
*
* @param mixed $model the data model to be rendered
* @param mixed $key the key value associated with the data model
* @param int $index the zero-based index of the data model in the model array returned by [[dataProvider]].
* @return string|null [[beforeItem]] call result or `null` when [[beforeItem]] is not a closure
* @see beforeItem
* @since 2.0.11
*/
protected function renderBeforeItem($model, $key, $index)
{
if ($this->beforeItem instanceof Closure) {
return call_user_func($this->beforeItem, $model, $key, $index, $this);
}
return null;
}
/**
* Calls [[afterItem]] closure, returns execution result.
* If [[afterItem]] is not a closure, `null` will be returned.
*
* @param mixed $model the data model to be rendered
* @param mixed $key the key value associated with the data model
* @param int $index the zero-based index of the data model in the model array returned by [[dataProvider]].
* @return string|null [[afterItem]] call result or `null` when [[afterItem]] is not a closure
* @see afterItem
* @since 2.0.11
*/
protected function renderAfterItem($model, $key, $index)
{
if ($this->afterItem instanceof Closure) {
return call_user_func($this->afterItem, $model, $key, $index, $this);
}
return null;
}
/**
* Renders a single data model.
* @param mixed $model the data model to be rendered
* @param mixed $key the key value associated with the data model

7
framework/widgets/Menu.php

@ -7,6 +7,7 @@
namespace yii\widgets;
use Closure;
use Yii;
use yii\base\Widget;
use yii\helpers\ArrayHelper;
@ -60,7 +61,9 @@ class Menu extends Widget
* otherwise, [[labelTemplate]] will be used.
* - visible: boolean, optional, whether this menu item is visible. Defaults to true.
* - items: array, optional, specifies the sub-menu items. Its format is the same as the parent items.
* - active: boolean, optional, whether this menu item is in active state (currently selected).
* - active: boolean or Closure, optional, whether this menu item is in active state (currently selected).
* When using a closure, its signature should be `function ($item, $hasActiveChild, $isItemActive, $widget)`.
* Closure must return `true` if item should be marked as `active`, otherwise - `false`.
* If a menu item is active, its CSS class will be appended with [[activeCssClass]].
* If this option is not set, the menu item will be set active automatically when the current request
* is triggered by `url`. For more details, please refer to [[isItemActive()]].
@ -285,6 +288,8 @@ class Menu extends Widget
} else {
$items[$i]['active'] = false;
}
} elseif ($item['active'] instanceof Closure) {
$active = $items[$i]['active'] = call_user_func($item['active'], $item, $hasActiveChild, $this->isItemActive($item), $this);
} elseif ($item['active']) {
$active = true;
}

9
framework/widgets/Pjax.php

@ -97,6 +97,15 @@ class Pjax extends Widget
* [pjax project page](https://github.com/yiisoft/jquery-pjax) for available options.
*/
public $clientOptions;
/**
* @inheritdoc
* @internal
*/
public static $counter = 0;
/**
* @inheritdoc
*/
public static $autoIdPrefix = 'p';
/**

5
tests/data/travis/imagick-setup.sh

@ -0,0 +1,5 @@
#!/bin/sh -e
if [ $(phpenv version-name) = '5.4' ] || [ $(phpenv version-name) = '5.5' ] || [ $(phpenv version-name) = '5.6' ]; then
yes '' | pecl install imagick
fi

63
tests/framework/console/RequestTest.php

@ -0,0 +1,63 @@
<?php
use yii\console\Request;
use yiiunit\TestCase;
/**
* @group console
*/
class RequestTest extends TestCase
{
public function provider()
{
return [
[
'params' => [
'controller',
],
'expected' => [
'route' => 'controller',
'params' => [
]
]
],
[
'params' => [
'controller/route',
'param1',
'-12345',
'--option1',
'--option2=testValue',
'-alias1',
'-alias2=testValue'
],
'expected' => [
'route' => 'controller/route',
'params' => [
'param1',
'-12345',
'option1' => '1',
'option2' => 'testValue',
'_aliases' => [
'alias1' => true,
'alias2' => 'testValue'
]
]
]
]
];
}
/**
* @dataProvider provider
*/
public function testResolve($params, $expected)
{
$request = new Request();
$request->setParams($params);
list($route, $params) = $request->resolve();
$this->assertEquals($expected['route'], $route);
$this->assertEquals($expected['params'], $params);
}
}

44
tests/framework/console/controllers/MigrateControllerTestTrait.php

@ -210,12 +210,12 @@ CODE;
public function testUp()
{
$this->createMigration('test1');
$this->createMigration('test2');
$this->createMigration('test_up1');
$this->createMigration('test_up2');
$this->runMigrateControllerAction('up');
$this->assertMigrationHistory(['m*_base', 'm*_test1', 'm*_test2']);
$this->assertMigrationHistory(['m*_base', 'm*_test_up1', 'm*_test_up2']);
}
/**
@ -223,12 +223,12 @@ CODE;
*/
public function testUpCount()
{
$this->createMigration('test1');
$this->createMigration('test2');
$this->createMigration('test_down1');
$this->createMigration('test_down2');
$this->runMigrateControllerAction('up', [1]);
$this->assertMigrationHistory(['m*_base', 'm*_test1']);
$this->assertMigrationHistory(['m*_base', 'm*_test_down1']);
}
/**
@ -236,13 +236,13 @@ CODE;
*/
public function testDownCount()
{
$this->createMigration('test1');
$this->createMigration('test2');
$this->createMigration('test_down_count1');
$this->createMigration('test_down_count2');
$this->runMigrateControllerAction('up');
$this->runMigrateControllerAction('down', [1]);
$this->assertMigrationHistory(['m*_base', 'm*_test1']);
$this->assertMigrationHistory(['m*_base', 'm*_test_down_count1']);
}
/**
@ -250,8 +250,8 @@ CODE;
*/
public function testDownAll()
{
$this->createMigration('test1');
$this->createMigration('test2');
$this->createMigration('test_down_all1');
$this->createMigration('test_down_all2');
$this->runMigrateControllerAction('up');
$this->runMigrateControllerAction('down', ['all']);
@ -267,13 +267,13 @@ CODE;
$output = $this->runMigrateControllerAction('history');
$this->assertContains('No migration', $output);
$this->createMigration('test1');
$this->createMigration('test2');
$this->createMigration('test_history1');
$this->createMigration('test_history2');
$this->runMigrateControllerAction('up');
$output = $this->runMigrateControllerAction('history');
$this->assertContains('_test1', $output);
$this->assertContains('_test2', $output);
$this->assertContains('_test_history1', $output);
$this->assertContains('_test_history2', $output);
}
/**
@ -281,25 +281,25 @@ CODE;
*/
public function testNew()
{
$this->createMigration('test1');
$this->createMigration('test_new1');
$output = $this->runMigrateControllerAction('new');
$this->assertContains('_test1', $output);
$this->assertContains('_test_new1', $output);
$this->runMigrateControllerAction('up');
$output = $this->runMigrateControllerAction('new');
$this->assertNotContains('_test1', $output);
$this->assertNotContains('_test_new1', $output);
}
public function testMark()
{
$version = '010101_000001';
$this->createMigration('test1', $version);
$this->createMigration('test_mark1', $version);
$this->runMigrateControllerAction('mark', [$version]);
$this->assertMigrationHistory(['m*_base', 'm*_test1']);
$this->assertMigrationHistory(['m*_base', 'm*_test_mark1']);
}
public function testTo()
@ -317,12 +317,12 @@ CODE;
*/
public function testRedo()
{
$this->createMigration('test1');
$this->createMigration('test_redo1');
$this->runMigrateControllerAction('up');
$this->runMigrateControllerAction('redo');
$this->assertMigrationHistory(['m*_base', 'm*_test1']);
$this->assertMigrationHistory(['m*_base', 'm*_test_redo1']);
}
// namespace :

63
tests/framework/db/QueryBuilderTest.php

@ -1582,10 +1582,65 @@ abstract class QueryBuilderTest extends DatabaseTestCase
// // TODO implement
// }
//
// public function testBatchInsert()
// {
// // TODO implement
// }
public function batchInsertProvider()
{
return [
[
'customer',
['email', 'name', 'address'],
[['test@example.com', 'silverfire', 'Kyiv {{city}}, Ukraine']],
$this->replaceQuotes("INSERT INTO [[customer]] ([[email]], [[name]], [[address]]) VALUES ('test@example.com', 'silverfire', 'Kyiv {{city}}, Ukraine')")
],
'escape-danger-chars' => [
'customer',
['address'],
[["SQL-danger chars are escaped: '); --"]],
'expected' => $this->replaceQuotes("INSERT INTO [[customer]] ([[address]]) VALUES ('SQL-danger chars are escaped: \'); --')")
],
[
'customer',
['address'],
[],
''
],
[
'customer',
[],
[["no columns passed"]],
$this->replaceQuotes("INSERT INTO [[customer]] () VALUES ('no columns passed')")
],
'bool-false, bool2-null' => [
'type',
['bool_col', 'bool_col2'],
[[false, null]],
'expected' => $this->replaceQuotes("INSERT INTO [[type]] ([[bool_col]], [[bool_col2]]) VALUES (0, NULL)")
],
[
'{{%type}}',
['{{%type}}.[[float_col]]', '[[time]]'],
[[null, new Expression('now()')]],
"INSERT INTO {{%type}} ({{%type}}.[[float_col]], [[time]]) VALUES (NULL, now())"
],
'bool-false, time-now()' => [
'{{%type}}',
['{{%type}}.[[bool_col]]', '[[time]]'],
[[false, new Expression('now()')]],
'expected' => "INSERT INTO {{%type}} ({{%type}}.[[bool_col]], [[time]]) VALUES (0, now())"
],
];
}
/**
* @dataProvider batchInsertProvider
*/
public function testBatchInsert($table, $columns, $value, $expected)
{
$queryBuilder = $this->getQueryBuilder();
$sql = $queryBuilder->batchInsert($table, $columns, $value);
$this->assertEquals($expected, $sql);
}
//
// public function testUpdate()
// {

8
tests/framework/db/QueryTest.php

@ -319,6 +319,10 @@ abstract class QueryTest extends DatabaseTestCase
$count = (new Query)->select('[[status]], COUNT([[id]])')->from('customer')->groupBy('status')->count('*', $db);
$this->assertEquals(2, $count);
// testing that orderBy() should be ignored here as it does not affect the count anyway.
$count = (new Query)->from('customer')->orderBy('status')->count('*', $db);
$this->assertEquals(3, $count);
}
/**
@ -344,11 +348,11 @@ abstract class QueryTest extends DatabaseTestCase
$query->andFilterCompare('name', 'Doe', 'like');
$this->assertEquals($condition, $query->where);
$condition = ['and', $condition, ['>', 'rating', '9']];
$condition[] = ['>', 'rating', '9'];
$query->andFilterCompare('rating', '>9');
$this->assertEquals($condition, $query->where);
$condition = ['and', $condition, ['<=', 'value', '100']];
$condition[] = ['<=', 'value', '100'];
$query->andFilterCompare('value', '<=100');
$this->assertEquals($condition, $query->where);
}

11
tests/framework/db/mssql/QueryBuilderTest.php

@ -89,4 +89,15 @@ class QueryBuilderTest extends \yiiunit\framework\db\QueryBuilderTest
{
return array_merge(parent::columnTypes(), []);
}
public function batchInsertProvider()
{
$data = parent::batchInsertProvider();
$data['escape-danger-chars']['expected'] = 'INSERT INTO [customer] ([address]) VALUES ("SQL-danger chars are escaped: \'); --")';
$data['bool-false, bool2-null']['expected'] = 'INSERT INTO [type] ([bool_col], [bool_col2]) VALUES (FALSE, NULL)';
$data['bool-false, time-now()']['expected'] = "INSERT INTO {{%type}} ({{%type}}.[[bool_col]], [[time]]) VALUES (FALSE, now())";
return $data;
}
}

11
tests/framework/db/pgsql/QueryBuilderTest.php

@ -120,4 +120,15 @@ class QueryBuilderTest extends \yiiunit\framework\db\QueryBuilderTest
$sql = $qb->dropCommentFromTable('comment');
$this->assertEquals($this->replaceQuotes($expected), $sql);
}
public function batchInsertProvider()
{
$data = parent::batchInsertProvider();
$data['escape-danger-chars']['expected'] = "INSERT INTO \"customer\" (\"address\") VALUES ('SQL-danger chars are escaped: ''); --')";
$data['bool-false, bool2-null']['expected'] = 'INSERT INTO "type" ("bool_col", "bool_col2") VALUES (FALSE, NULL)';
$data['bool-false, time-now()']['expected'] = "INSERT INTO {{%type}} ({{%type}}.[[bool_col]], [[time]]) VALUES (FALSE, now())";
return $data;
}
}

25
tests/framework/di/ServiceLocatorTest.php

@ -86,4 +86,29 @@ class ServiceLocatorTest extends TestCase
$this->assertTrue($object2 instanceof $className);
$this->assertTrue($object === $object2);
}
/**
* https://github.com/yiisoft/yii2/issues/11771
*/
public function testModulePropertyIsset()
{
$config = [
'components' => [
'captcha' => [
'name' => 'foo bar',
'class' => 'yii\captcha\Captcha',
],
],
];
$app = new ServiceLocator($config);
$this->assertTrue(isset($app->captcha->name));
$this->assertFalse(empty($app->captcha->name));
$this->assertEquals('foo bar', $app->captcha->name);
$this->assertTrue(isset($app->captcha->name));
$this->assertFalse(empty($app->captcha->name));
}
}

26
tests/framework/helpers/ArrayHelperTest.php

@ -298,6 +298,32 @@ class ArrayHelperTest extends TestCase
$this->assertEquals(['name' => 'b', 'age' => 3], $array[2]);
}
public function testMultisortClosure()
{
$changelog = [
'- Enh #123: test1',
'- Bug #125: test2',
'- Bug #123: test2',
'- Enh: test3',
'- Bug: test4',
];
$i = 0;
ArrayHelper::multisort($changelog, function($line) use (&$i) {
if (preg_match('/^- (Enh|Bug)( #\d+)?: .+$/', $line, $m)) {
$o = ['Bug' => 'C', 'Enh' => 'D'];
return $o[$m[1]] . ' ' . (!empty($m[2]) ? $m[2] : 'AAAA' . $i++);
}
return 'B' . $i++;
}, SORT_ASC, SORT_NATURAL);
$this->assertEquals([
'- Bug #123: test2',
'- Bug #125: test2',
'- Bug: test4',
'- Enh #123: test1',
'- Enh: test3',
], $changelog);
}
public function testMerge()
{
$a = [

59
tests/framework/helpers/FileHelperTest.php

@ -61,10 +61,11 @@ class FileHelperTest extends TestCase
if ($handle = opendir($dirName)) {
while (false !== ($entry = readdir($handle))) {
if ($entry != '.' && $entry != '..') {
if (is_dir($dirName . DIRECTORY_SEPARATOR . $entry) === true) {
$this->removeDir($dirName . DIRECTORY_SEPARATOR . $entry);
$item = $dirName . DIRECTORY_SEPARATOR . $entry;
if (is_dir($item) === true && !is_link($item)) {
$this->removeDir($item);
} else {
unlink($dirName . DIRECTORY_SEPARATOR . $entry);
unlink($item);
}
}
}
@ -515,6 +516,58 @@ class FileHelperTest extends TestCase
/**
* @depends testFindFiles
*/
public function testFindFilesRecursiveWithSymLink()
{
$dirName = 'test_dir';
$this->createFileStructure([
$dirName => [
'theDir' => [
'file1' => 'abc',
'file2' => 'def',
],
'symDir' => ['symlink', 'theDir'],
],
]);
$dirName = $this->testFilePath . DIRECTORY_SEPARATOR . $dirName;
$expected = [
$dirName . DIRECTORY_SEPARATOR . 'symDir' . DIRECTORY_SEPARATOR . 'file1',
$dirName . DIRECTORY_SEPARATOR . 'symDir' . DIRECTORY_SEPARATOR . 'file2',
$dirName . DIRECTORY_SEPARATOR . 'theDir' . DIRECTORY_SEPARATOR . 'file1',
$dirName . DIRECTORY_SEPARATOR . 'theDir' . DIRECTORY_SEPARATOR . 'file2',
];
$result = FileHelper::findFiles($dirName);
sort($result);
$this->assertEquals($expected, $result);
}
/**
* @depends testFindFiles
*/
public function testFindFilesNotRecursive()
{
$dirName = 'test_dir';
$this->createFileStructure([
$dirName => [
'theDir' => [
'file1' => 'abc',
'file2' => 'def',
],
'symDir' => ['symlink', 'theDir'],
'file3' => 'root'
],
]);
$dirName = $this->testFilePath . DIRECTORY_SEPARATOR . $dirName;
$expected = [
$dirName . DIRECTORY_SEPARATOR . 'file3',
];
$this->assertEquals($expected, FileHelper::findFiles($dirName, ['recursive' => false]));
}
/**
* @depends testFindFiles
*/
public function testFindFilesExclude()
{
$basePath = $this->testFilePath . DIRECTORY_SEPARATOR;

10
tests/framework/rbac/ManagerTestCase.php

@ -182,6 +182,16 @@ abstract class ManagerTestCase extends TestCase
'blablabla' => false,
null => false,
],
'guest' => [
// all actions denied for guest (user not exists)
'createPost' => false,
'readPost' => false,
'updatePost' => false,
'deletePost' => false,
'updateAnyPost' => false,
'blablabla' => false,
null => false,
],
];
$params = ['authorID' => 'author B'];

124
tests/framework/rest/UrlRuleTest.php

@ -200,8 +200,9 @@ class UrlRuleTest extends TestCase
* Proviedes test cases for createUrl() method
*
* - first param are properties of the UrlRule
* - second param is the route to create
* - third param is the expected URL
* - second param is an array of test cases, containing two element arrays:
* - first element is the route to create
* - second element is the expected URL
*/
public function createUrlDataProvider()
{
@ -212,40 +213,46 @@ class UrlRuleTest extends TestCase
'controller' => 'v1/channel',
'pluralize' => true,
],
['v1/channel/index'], // route
'v1/channels', // expected
[ // test cases: route, expected
[ ['v1/channel/index'], 'v1/channels' ],
[ ['v1/channel/index', 'offset' => 1], 'v1/channels?offset=1' ],
[ ['v1/channel/view', 'id' => 42], 'v1/channels/42' ],
[ ['v1/channel/options'], 'v1/channels' ],
[ ['v1/channel/options', 'id' => 42], 'v1/channels/42' ],
[ ['v1/channel/delete'], false ],
],
],
[
[ // Rule properties
'controller' => ['v1/channel'],
'pluralize' => true,
],
['v1/channel/index'], // route
'v1/channels', // expected
],
[
[ // Rule properties
'controller' => ['v1/channel', 'v1/u' => 'v1/user'],
'pluralize' => true,
[ // test cases: route, expected
[ ['v1/channel/index'], 'v1/channels' ],
[ ['v1/channel/index', 'offset' => 1], 'v1/channels?offset=1' ],
[ ['v1/channel/view', 'id' => 42], 'v1/channels/42' ],
[ ['v1/channel/options'], 'v1/channels' ],
[ ['v1/channel/options', 'id' => 42], 'v1/channels/42' ],
[ ['v1/channel/delete'], false ],
],
['v1/channel/index'], // route
'v1/channels', // expected
],
[
[ // Rule properties
'controller' => ['v1/channel', 'v1/u' => 'v1/user'],
'pluralize' => true,
],
['v1/user/index'], // route
'v1/u', // expected
[ // test cases: route, expected
[ ['v1/channel/index'], 'v1/channels' ],
[ ['v1/channel/view', 'id' => 42], 'v1/channels/42' ],
[ ['v1/channel/options'], 'v1/channels' ],
[ ['v1/channel/options', 'id' => 42], 'v1/channels/42' ],
[ ['v1/channel/delete'], false ],
[ ['v1/user/index'], 'v1/u' ],
[ ['v1/user/view', 'id' => 1], 'v1/u/1' ],
[ ['v1/channel/options'], 'v1/channels' ],
[ ['v1/channel/options', 'id' => 42], 'v1/channels/42' ],
[ ['v1/user/delete'], false ],
],
[
[ // Rule properties
'controller' => 'v1/channel',
'pluralize' => true,
],
['v1/channel/index', 'offset' => 1], // route
'v1/channels?offset=1', // expected
],
@ -255,51 +262,87 @@ class UrlRuleTest extends TestCase
'controller' => 'v1/channel',
'pluralize' => false,
],
['v1/channel/index'], // route
'v1/channel', // expected
[ // test cases: route, expected
[ ['v1/channel/index'], 'v1/channel' ],
[ ['v1/channel/index', 'offset' => 1], 'v1/channel?offset=1' ],
[ ['v1/channel/view', 'id' => 42], 'v1/channel/42' ],
[ ['v1/channel/options'], 'v1/channel' ],
[ ['v1/channel/options', 'id' => 42], 'v1/channel/42' ],
[ ['v1/channel/delete'], false ],
],
],
[
[ // Rule properties
'controller' => ['v1/channel'],
'pluralize' => false,
],
['v1/channel/index'], // route
'v1/channel', // expected
[ // test cases: route, expected
[ ['v1/channel/index'], 'v1/channel' ],
[ ['v1/channel/index', 'offset' => 1], 'v1/channel?offset=1' ],
[ ['v1/channel/view', 'id' => 42], 'v1/channel/42' ],
[ ['v1/channel/options'], 'v1/channel' ],
[ ['v1/channel/options', 'id' => 42], 'v1/channel/42' ],
[ ['v1/channel/delete'], false ],
],
[
[ // Rule properties
'controller' => ['v1/channel', 'v1/u' => 'v1/user'],
'pluralize' => false,
],
['v1/channel/index'], // route
'v1/channel', // expected
],
[
[ // Rule properties
'controller' => ['v1/channel', 'v1/u' => 'v1/user'],
'pluralize' => false,
],
['v1/user/index'], // route
'v1/u', // expected
[ // test cases: route, expected
[ ['v1/channel/index'], 'v1/channel' ],
[ ['v1/channel/view', 'id' => 42], 'v1/channel/42' ],
[ ['v1/channel/options'], 'v1/channel' ],
[ ['v1/channel/options', 'id' => 42], 'v1/channel/42' ],
[ ['v1/channel/delete'], false ],
[ ['v1/user/index'], 'v1/u' ],
[ ['v1/user/view', 'id' => 1], 'v1/u/1' ],
[ ['v1/user/options'], 'v1/u' ],
[ ['v1/user/options', 'id' => 42], 'v1/u/42' ],
[ ['v1/user/delete'], false ],
],
],
// using extra patterns
[
[ // Rule properties
'controller' => 'v1/channel',
'pluralize' => false,
'pluralize' => true,
'extraPatterns' => [
'{id}/my' => 'my',
'my' => 'my',
// this should not create a URL, no GET definition
'POST {id}/my2' => 'my2',
],
],
[ // test cases: route, expected
// normal actions should behave as before
[ ['v1/channel/index'], 'v1/channels' ],
[ ['v1/channel/index', 'offset' => 1], 'v1/channels?offset=1' ],
[ ['v1/channel/view', 'id' => 42], 'v1/channels/42' ],
[ ['v1/channel/options'], 'v1/channels' ],
[ ['v1/channel/options', 'id' => 42], 'v1/channels/42' ],
[ ['v1/channel/delete'], false ],
[ ['v1/channel/my'], 'v1/channels/my' ],
[ ['v1/channel/my', 'id' => 42], 'v1/channels/42/my' ],
[ ['v1/channel/my2'], false ],
[ ['v1/channel/my2', 'id' => 42], false ],
],
['v1/channel/index', 'offset' => 1], // route
'v1/channel?offset=1', // expected
],
// ---
];
}
/**
* @dataProvider createUrlDataProvider
*/
public function testCreateUrl($rule, $params, $expected)
public function testCreateUrl($rule, $tests)
{
foreach($tests as $test) {
list($params, $expected) = $test;
$this->mockWebApplication();
Yii::$app->set('request', new Request(['hostInfo' => 'http://api.example.com', 'scriptUrl' => '/index.php']));
$route = array_shift($params);
@ -310,5 +353,6 @@ class UrlRuleTest extends TestCase
$rule = new UrlRule($rule);
$this->assertEquals($expected, $rule->createUrl($manager, $route, $params));
}
}
}

10
tests/framework/validators/CompareValidatorTest.php

@ -83,11 +83,11 @@ class CompareValidatorTest extends TestCase
[$value + 1, false],
[$value - 1, true],
],
//'non-op' => [
// [$value, false],
// [$value + 1, false],
// [$value - 1, false],
//],
/*'non-op' => [
[$value, false],
[$value + 1, false],
[$value - 1, false],
],*/
];
}

8
tests/framework/widgets/ActiveFieldTest.php

@ -497,6 +497,14 @@ EOD;
$this->assertEqualsWithoutLE($expectedValue, $actualValue);
}
public function testEmptyTag()
{
$this->activeField->options = ['tag' => false];
$expectedValue = '<input type="hidden" id="activefieldtestmodel-attributename" class="form-control" name="ActiveFieldTestModel[attributeName]">';
$actualValue = $this->activeField->hiddenInput()->label(false)->error(false)->hint(false)->render();
$this->assertEqualsWithoutLE($expectedValue, trim($actualValue));
}
/**
* Helper methods
*/

33
tests/framework/widgets/ListViewTest.php

@ -170,4 +170,37 @@ HTML
$this->getListView(['itemOptions' => $itemOptions])->run();
$this->expectOutputString($expected);
}
public function testBeforeAndAfterItem()
{
$before = function ($model, $key, $index, $widget) {
$widget = get_class($widget);
return "<!-- before: {$model['id']}, key: $key, index: $index, widget: $widget -->";
};
$after = function ($model, $key, $index, $widget) {
if ($model['id'] === 1) {
return null;
}
$widget = get_class($widget);
return "<!-- after: {$model['id']}, key: $key, index: $index, widget: $widget -->";
};
$this->getListView([
'beforeItem' => $before,
'afterItem' => $after
])->run();
$this->expectOutputString(<<<HTML
<div id="w0" class="list-view"><div class="summary">Showing <b>1-3</b> of <b>3</b> items.</div>
<!-- before: 1, key: 0, index: 0, widget: yii\widgets\ListView -->
<div data-key="0">0</div>
<!-- before: 2, key: 1, index: 1, widget: yii\widgets\ListView -->
<div data-key="1">1</div>
<!-- after: 2, key: 1, index: 1, widget: yii\widgets\ListView -->
<!-- before: 3, key: 2, index: 2, widget: yii\widgets\ListView -->
<div data-key="2">2</div>
<!-- after: 3, key: 2, index: 2, widget: yii\widgets\ListView -->
</div>
HTML
);
}
}

41
tests/framework/widgets/MenuTest.php

@ -2,6 +2,7 @@
namespace yiiunit\framework\widgets;
use Yii;
use yii\widgets\Menu;
/**
@ -158,5 +159,45 @@ HTML;
$this->assertEqualsWithoutLE($expected, $output);
}
public function testActiveItemClosure()
{
$output = Menu::widget([
'route' => 'test/test',
'params' => [],
'linkTemplate' => '',
'labelTemplate' => '',
'items' => [
[
'label' => 'item1',
'url' => '#',
'template' => 'label: {label}; url: {url}',
'active' => function ($item, $hasActiveChild, $isItemActive, $widget) {
return isset($item, $hasActiveChild, $isItemActive, $widget);
}
],
[
'label' => 'item2',
'template' => 'label: {label}',
'active' => false
],
[
'label' => 'item3 (no template)',
'active' => 'somestring'
],
]
]);
$expected = <<<HTML
<ul><li class="active">label: item1; url: #</li>
<li>label: item2</li>
<li class="active"></li></ul>
HTML;
$this->assertEqualsWithoutLE($expected, $output);
}
public function testIsItemActive()
{
// TODO: implement test of protected method isItemActive()
}
}

37
tests/framework/widgets/PjaxTest.php

@ -0,0 +1,37 @@
<?php
namespace yiiunit\framework\widgets;
use yii\data\ArrayDataProvider;
use yii\widgets\ListView;
use yii\widgets\Pjax;
use yiiunit\TestCase;
class PjaxTest extends TestCase
{
public function testGeneratedIdByPjaxWidget()
{
ListView::$counter = 0;
Pjax::$counter = 0;
$nonPjaxWidget1 = new ListView(['dataProvider' => new ArrayDataProvider()]);
ob_start();
$pjax1 = new Pjax();
ob_end_clean();
$nonPjaxWidget2 = new ListView(['dataProvider' => new ArrayDataProvider()]);
ob_start();
$pjax2 = new Pjax();
ob_end_clean();
$this->assertEquals('w0', $nonPjaxWidget1->options['id']);
$this->assertEquals('w1', $nonPjaxWidget2->options['id']);
$this->assertEquals('p0', $pjax1->options['id']);
$this->assertEquals('p1', $pjax2->options['id']);
}
protected function setUp()
{
parent::setUp();
$this->mockWebApplication();
}
}

170
tests/js/data/yii.gridView.html

@ -0,0 +1,170 @@
<!-- Filters for testing of multiple grid views -->
<div id="w-common-filters">
<input name="PostSearch[id]" type="text">
<input name="PostSearch[name]" type="text">
</div>
<!-- The main setup -->
<div id="w0" class="grid-view">
<table>
<thead>
<tr>
<th><input id="w0-check-all" name="selection_all" value="1" type="checkbox"></th>
<th>Name</th>
<th>Category</th>
<th>Tags</th>
</tr>
<tr id="w0-filters">
<td>&nbsp;</td>
<td><input id="w0-name" name="PostSearch[name]" type="text"></td>
<td>
<select id="w0-category" name="PostSearch[category_id]">
<option value="" selected>None</option>
<option value="1">Programming</option>
<option value="2">Traveling</option>
</select>
</td>
<td>
<select id="w0-tags" name="PostSearch[tags][]" multiple>
<option value="1">html</option>
<option value="2">css</option>
<option value="3">js</option>
<option value="4">php</option>
</select>
</td>
</tr>
</thead>
<tbody>
<tr data-key="1">
<td><input class="w0-check-row" name="selection[]" value="1" type="checkbox"></td>
<td>Name 1</td>
<td>Programming</td>
<td>html, css</td>
</tr>
<tr data-key="2">
<td><input class="w0-check-row" name="selection[]" value="2" type="checkbox"></td>
<td>Name 2</td>
<td>Programming</td>
<td>js</td>
</tr>
<tr data-key="3">
<td><input class="w0-check-row" name="selection[]" value="3" type="checkbox"></td>
<td>Name 3</td>
<td>Programming</td>
<td>php</td>
</tr>
</tbody>
</table>
</div>
<!-- The basic setup, used for testing of multiple grid views -->
<div id="w1" class="grid-view">
<table>
<thead>
<tr>
<th><input name="selection_all" value="1" type="checkbox"></th>
<th>ID</th>
<th>Name</th>
</tr>
<tr id="w1-filters">
<td>&nbsp;</td>
<td><input name="PostSearch[id]" type="text"></td>
<td><input name="PostSearch[name]" type="text"></td>
</tr>
</thead>
<tbody>
<tr data-key="1">
<td><input name="selection[]" value="1" type="checkbox"></td>
<td>1</td>
<td>Name 1</td>
</tr>
<tr data-key="2">
<td><input name="selection[]" value="2" type="checkbox"></td>
<td>2</td>
<td>Name 2</td>
</tr>
</tbody>
</table>
</div>
<!-- https://github.com/yiisoft/yii2/pull/10284 -->
<div id="w2">
<table>
<thead>
<tr>
<th>Name</th>
<th>Tags</th>
</tr>
<tr id="w2-filters">
<td><input name="PostSearch[name]" type="text"></td>
<td>
<input type="hidden" name="PostSearch[tags]" value="-1">
<select id="w2-tags" name="PostSearch[tags][]" multiple>
<option value="1">html</option>
<option value="2">css</option>
<option value="3">js</option>
<option value="4">php</option>
</select>
</td>
</tr>
</thead>
<tbody>
<tr data-key="1">
<td>Name 1</td>
<td>html, css</td>
</tr>
<tr data-key="2">
<td>Name 2</td>
<td>js</td>
</tr>
<tr data-key="3">
<td>Name 3</td>
<td>php</td>
</tr>
</tbody>
</table>
</div>
<!-- Setup for testing that event handlers are correctly removed with new selectors -->
<div id="w3">
<table>
<thead>
<tr>
<th>
<input name="selection_all" value="1" type="checkbox">
<input name="selection_all2" value="1" type="checkbox">
</th>
<th>ID</th>
<th>Name</th>
</tr>
<tr id="w3-filters">
<td>&nbsp;</td>
<td><input name="PostSearch[id]" type="text"></td>
<td><input name="PostSearch[name]" type="text"></td>
</tr>
</thead>
<tbody>
<tr data-key="1">
<td>
<input class="w3-check-row" name="selection[]" value="1" type="checkbox">
<input name="selection2[]" value="1" type="checkbox">
</td>
<td>1</td>
<td>Name 1</td>
</tr>
<tr data-key="2">
<td>
<input class="w3-check-row" name="selection[]" value="2" type="checkbox">
<input name="selection2[]" value="2" type="checkbox">
</td>
<td>2</td>
<td>Name 2</td>
</tr>
</tbody>
</table>
</div>

754
tests/js/tests/yii.gridView.test.js

@ -0,0 +1,754 @@
var assert = require('chai').assert;
var sinon;
var withData = require('leche').withData;
var jsdom = require('mocha-jsdom');
var fs = require('fs');
var vm = require('vm');
describe('yii.gridView', function () {
var yiiGridViewPath = 'framework/assets/yii.gridView.js';
var yiiPath = 'framework/assets/yii.js';
var jQueryPath = 'vendor/bower/jquery/dist/jquery.js';
var $;
var $gridView;
var settings = {
filterUrl: '/posts/index',
filterSelector: '#w0-filters input, #w0-filters select'
};
var commonSettings = {
filterUrl: '/posts/index',
filterSelector: '#w-common-filters input, #w-common-filters select'
};
var $textInput;
var $select;
var $multipleSelect;
var $listBox;
var $checkAllCheckbox;
var $checkRowCheckboxes;
function registerYii() {
var code = fs.readFileSync(yiiPath);
var script = new vm.Script(code);
var sandbox = {window: window, jQuery: $};
var context = new vm.createContext(sandbox);
script.runInContext(context);
return sandbox.window.yii;
}
function registerTestableCode() {
var yii = registerYii();
var code = fs.readFileSync(yiiGridViewPath);
var script = new vm.Script(code);
var context = new vm.createContext({window: window, document: window.document, yii: yii});
script.runInContext(context);
}
var gridViewHtml = fs.readFileSync('tests/js/data/yii.gridView.html', 'utf-8');
var html = '<!doctype html><html><head><meta charset="utf-8"></head><body>' + gridViewHtml + '</body></html>';
jsdom({
html: html,
src: fs.readFileSync(jQueryPath, 'utf-8')
});
before(function () {
$ = window.$;
registerTestableCode();
sinon = require('sinon');
});
beforeEach(function () {
$textInput = $('#w0-name');
$select = $('#w0-category');
$multipleSelect = $('#w0-tags');
$listBox = $('#w2-tags');
$checkAllCheckbox = $('#w0-check-all');
$checkRowCheckboxes = $('.w0-check-row');
});
afterEach(function () {
if ($gridView.length) {
$gridView.yiiGridView('destroy');
}
$textInput.val('');
$select.val('');
$multipleSelect.find('option:selected').prop('selected', false);
$listBox.find('option:selected').prop('selected', false);
$checkAllCheckbox.prop('checked', false);
$checkRowCheckboxes.prop('checked', false);
});
/**
* Simulate pressing "Enter" button while focused on some element
* @param $el
*/
function pressEnter($el) {
var e = $.Event('keydown', {keyCode: 13});
$el.trigger(e);
}
/**
* Simulate pressing keyboard button while focused on the text input. For simplicity, intended to use with letter
* buttons, such as "a", "b", etc. Case insensitive.
* @param $el
* @param buttonName
*/
function pressButton($el, buttonName) {
$el.val(buttonName);
var keyCode = buttonName.charCodeAt(0);
var e = $.Event('keydown', {keyCode: keyCode});
$el.trigger(e);
}
/**
* Simulate changing value in the select
* @param $el
* @param value
*/
function changeValue($el, value) {
$el.val(value);
var e = $.Event('change');
$el.trigger(e);
}
/**
* Simulate losing focus of the element after the value was changed
* @param $el
*/
function loseFocus($el) {
var e = $.Event('change');
$el.trigger(e);
}
/**
* Simulate click in the checkbox
* @param $el
*/
function click($el) {
var e = $.Event('click');
$el.trigger(e);
}
/**
* Simulate hovering on the new value and pressing "Enter" button in the select
* @param $el
*/
function hoverAndPressEnter($el) {
pressEnter($el);
// After pressing enter while hovering the value will be immediately changed as well like with losing focus
loseFocus($el);
}
describe('init', function () {
var customSettings = {
filterUrl: '/posts/filter',
filterSelector: '#w-common-filters input'
};
withData({
'no method specified': [function () {
$gridView = $('.grid-view').yiiGridView(commonSettings);
}, commonSettings],
'no method specified, custom settings': [function () {
$gridView = $('.grid-view').yiiGridView(customSettings);
}, customSettings],
'manual method call': [function () {
$gridView = $('.grid-view').yiiGridView('init', commonSettings);
}, commonSettings]
}, function (initFunction, expectedSettings) {
it('should save settings for all elements', function () {
initFunction();
assert.deepEqual($('#w0').yiiGridView('data'), {settings: expectedSettings});
assert.deepEqual($('#w1').yiiGridView('data'), {settings: expectedSettings});
});
});
describe('with repeated call', function () {
var jQuerySubmitStub;
before(function () {
jQuerySubmitStub = sinon.stub($.fn, 'submit');
});
after(function () {
jQuerySubmitStub.restore();
});
it('should remove "filter" event handler', function () {
$gridView = $('#w0').yiiGridView(settings);
$gridView.yiiGridView(settings);
// Change selector to make sure event handlers are removed regardless of the selector
$gridView.yiiGridView({
filterUrl: '/posts/index',
filterSelector: '#w0-filters select'
});
pressEnter($textInput);
assert.isFalse(jQuerySubmitStub.called);
changeValue($select, 1);
assert.isTrue(jQuerySubmitStub.calledOnce);
});
});
});
describe('applyFilter', function () {
var jQuerySubmit = function () {
};
var jQuerySubmitStub;
beforeEach(function () {
jQuerySubmitStub = sinon.stub($.fn, 'submit', jQuerySubmit);
});
afterEach(function () {
jQuerySubmitStub.restore();
});
describe('with beforeFilter returning not false', function () {
var calledMethods = []; // For testing the order of called methods
var beforeFilterSpy;
var afterFilterSpy;
before(function () {
jQuerySubmit = function () {
calledMethods.push('submit');
return this;
};
beforeFilterSpy = sinon.spy(function () {
calledMethods.push('beforeFilter');
});
afterFilterSpy = sinon.spy(function () {
calledMethods.push('afterFilter');
});
});
after(function () {
jQuerySubmit = function () {
};
beforeFilterSpy.reset();
afterFilterSpy.reset();
calledMethods = [];
});
var message = 'should send the request to correct url with correct parameters and apply events in ' +
'correct order';
it(message, function () {
$gridView = $('#w0').yiiGridView(settings)
.on('beforeFilter', beforeFilterSpy)
.on('afterFilter', afterFilterSpy);
$textInput.val('a');
$select.val(1);
$multipleSelect.find('option[value="1"]').prop('selected', true);
$multipleSelect.find('option[value="2"]').prop('selected', true);
$gridView.yiiGridView('applyFilter');
var expectedHtml = '<form action="/posts/index" method="get" class="gridview-filter-form" ' +
'style="display:none" data-pjax="">' +
'<input type="hidden" name="PostSearch[name]" value="a">' +
'<input type="hidden" name="PostSearch[category_id]" value="1">' +
'<input type="hidden" name="PostSearch[tags][]" value="1">' +
'<input type="hidden" name="PostSearch[tags][]" value="2">' +
'</form>';
var $form = $('.grid-view .gridview-filter-form');
assert.equal($form.get(0).outerHTML, expectedHtml);
assert.isTrue(beforeFilterSpy.calledOnce);
assert.instanceOf(beforeFilterSpy.getCall(0).args[0], $.Event);
assert.equal($(beforeFilterSpy.getCall(0).args[0].target).attr('id'), $gridView.attr('id'));
assert.isTrue(jQuerySubmitStub.calledOnce);
assert.equal(jQuerySubmitStub.returnValues[0].attr('class'), 'gridview-filter-form');
assert.isTrue(afterFilterSpy.calledOnce);
assert.instanceOf(afterFilterSpy.getCall(0).args[0], $.Event);
assert.equal($(afterFilterSpy.getCall(0).args[0].target).attr('id'), $gridView.attr('id'));
assert.deepEqual(calledMethods, ['beforeFilter', 'submit', 'afterFilter']);
});
});
describe('with beforeFilter returning false', function () {
var beforeFilterSpy;
var afterFilterSpy;
before(function () {
beforeFilterSpy = sinon.spy(function () {
return false;
});
afterFilterSpy = sinon.spy();
});
after(function () {
beforeFilterSpy.reset();
afterFilterSpy.reset();
});
it('should prevent from sending request and triggering "afterFilter" event', function () {
$gridView = $('#w0').yiiGridView(settings)
.on('beforeFilter', beforeFilterSpy)
.on('afterFilter', afterFilterSpy);
$gridView.yiiGridView('applyFilter');
assert.isTrue(beforeFilterSpy.calledOnce);
assert.isFalse(jQuerySubmitStub.called);
assert.isFalse(afterFilterSpy.called);
});
});
describe('with different urls', function () {
describe('with no filter data sent', function () {
withData({
'query parameters': [
'/posts/index?foo=1&bar=2',
'/posts/index',
'PostSearch[name]=&PostSearch[category_id]=&foo=1&bar=2'
],
// https://github.com/yiisoft/yii2/pull/10302
'query parameter with multiple values (not array)': [
'/posts/index?foo=1&foo=2',
'/posts/index',
'PostSearch[name]=&PostSearch[category_id]=&foo=1&foo=2'
],
'query parameter with multiple values (array)': [
'/posts/index?foo[]=1&foo[]=2',
'/posts/index',
'PostSearch[name]=&PostSearch[category_id]=&foo[]=1&foo[]=2'
],
// https://github.com/yiisoft/yii2/issues/12836
'anchor': [
'/posts/index#post',
'/posts/index#post',
'PostSearch[name]=&PostSearch[category_id]='
],
'query parameters, anchor': [
'/posts/index?foo=1&bar=2#post',
'/posts/index#post',
'PostSearch[name]=&PostSearch[category_id]=&foo=1&bar=2'
],
'relative url, query parameters': [
'?foo=1&bar=2',
'',
'PostSearch[name]=&PostSearch[category_id]=&foo=1&bar=2'
],
'relative url, anchor': [
'#post',
'#post',
'PostSearch[name]=&PostSearch[category_id]='
],
'relative url, query parameters, anchor': [
'?foo=1&bar=2#post',
'#post',
'PostSearch[name]=&PostSearch[category_id]=&foo=1&bar=2'
]
}, function (filterUrl, expectedUrl, expectedQueryString) {
it('should send the request to correct url with correct parameters', function () {
var customSettings = $.extend({}, settings, {filterUrl: filterUrl});
$gridView = $('#w0').yiiGridView(customSettings);
$gridView.yiiGridView('applyFilter');
var $form = $gridView.find('.gridview-filter-form');
assert.isTrue(jQuerySubmitStub.calledOnce);
assert.equal($form.attr('action'), expectedUrl);
assert.equal(decodeURIComponent($form.serialize()), expectedQueryString);
});
});
});
// https://github.com/yiisoft/yii2/pull/10302
describe('with filter data sent', function () {
it('should send the request to correct url with new parameter values', function () {
var filterUrl = '/posts/index?CategorySearch[id]=5&CategorySearch[name]=c' +
'&PostSearch[name]=a&PostSearch[category_id]=1&PostSearch[tags][]=1&PostSearch[tags][]=2' +
'&foo[]=1&foo[]=2&bar=1#post';
var customSettings = $.extend({}, settings, {filterUrl: filterUrl});
$gridView = $('#w0').yiiGridView(customSettings);
$textInput.val('b');
$select.val('1'); // Leave value as is (simulate setting "selected" in HTML)
$multipleSelect.find('option[value="2"]').prop('selected', true);
$multipleSelect.find('option[value="3"]').prop('selected', true);
$gridView.yiiGridView('applyFilter');
var $form = $gridView.find('.gridview-filter-form');
assert.isTrue(jQuerySubmitStub.calledOnce);
assert.equal($form.attr('action'), '/posts/index#post');
// Parameters not related with current filter are appended to the end
var expectedQueryString = 'PostSearch[name]=b&PostSearch[category_id]=1' +
'&PostSearch[tags][]=2&PostSearch[tags][]=3' +
'&CategorySearch[id]=5&CategorySearch[name]=c' +
'&foo[]=1&foo[]=2&bar=1';
assert.equal(decodeURIComponent($form.serialize()), expectedQueryString);
});
});
});
// https://github.com/yiisoft/yii2/pull/10284
describe('with list box', function () {
var queryString = 'PostSearch[name]=&PostSearch[tags]=-1&PostSearch[tags][]=1&PostSearch[tags][]=2';
beforeEach(function () {
$listBox.find('option[value="1"]').prop('selected', true);
$listBox.find('option[value="2"]').prop('selected', true);
});
describe('with values selected', function () {
it('should send the request to correct url with correct parameters', function () {
$gridView = $('#w2').yiiGridView({
filterUrl: '/posts/index',
filterSelector: '#w2-filters input, #w2-filters select'
});
$gridView.yiiGridView('applyFilter');
var $form = $gridView.find('.gridview-filter-form');
assert.equal($form.attr('action'), '/posts/index');
assert.equal(decodeURIComponent($form.serialize()), queryString);
});
});
describe('with unselected values after applied filter', function () {
it('should send the request to correct url with correct parameters', function () {
$gridView = $('#w2').yiiGridView({
filterUrl: '/posts/index/?' + queryString,
filterSelector: '#w2-filters input, #w2-filters select'
});
$listBox.find('option:selected').prop('selected', false);
$gridView.yiiGridView('applyFilter');
var $form = $gridView.find('.gridview-filter-form');
assert.equal($form.attr('action'), '/posts/index/');
assert.equal(decodeURIComponent($form.serialize()), 'PostSearch[name]=&PostSearch[tags]=-1');
});
});
});
describe('with repeated method call', function () {
it('should delete the hidden form', function () {
$gridView = $('#w0').yiiGridView(settings);
$gridView.yiiGridView('applyFilter');
$gridView.yiiGridView('applyFilter');
var $form = $gridView.find('.gridview-filter-form');
assert.lengthOf($form, 1);
});
});
describe('with filter event handlers', function () {
beforeEach(function () {
$gridView = $('#w0').yiiGridView(settings);
});
describe('with text entered in the text input', function () {
it('should not submit form', function () {
pressButton($textInput, 'a');
assert.isFalse(jQuerySubmitStub.called);
});
});
describe('with "Enter" pressed in the text input', function () {
it('should submit form once', function () {
pressEnter($textInput);
assert.isTrue(jQuerySubmitStub.calledOnce);
});
});
describe('with text entered in the text input and lost focus', function () {
it('should submit form once', function () {
pressButton($textInput, 'a');
loseFocus($textInput);
assert.isTrue(jQuerySubmitStub.calledOnce);
});
});
describe('with value changed in the select', function () {
it('should submit form once', function () {
changeValue($select, 1);
assert.isTrue(jQuerySubmitStub.calledOnce);
});
});
describe('with hover on different value and "Enter" pressed in select', function () {
it('should submit form once', function () {
// Simulate hovering on new value and pressing "Enter"
$select.val(1);
hoverAndPressEnter($select);
assert.isTrue(jQuerySubmitStub.calledOnce);
});
});
});
});
describe('setSelectionColumn method', function () {
describe('with name option and', function () {
withData({
'nothing else': [{}],
'checkAll option': [{checkAll: 'selection_all'}],
'multiple option set to true': [{multiple: true}],
'multiple and checkAll options, multiple set to false': [{multiple: false, checkAll: 'selection_all'}]
}, function (customOptions) {
it('should update data and do not activate "check all" functionality', function () {
$gridView = $('#w0').yiiGridView(settings);
var defaultOptions = {name: 'selection[]'};
var options = $.extend({}, defaultOptions, customOptions);
$gridView.yiiGridView('setSelectionColumn', options);
assert.equal($gridView.yiiGridView('data').selectionColumn, 'selection[]');
click($checkAllCheckbox);
assert.lengthOf($checkRowCheckboxes.filter(':checked'), 0);
click($checkAllCheckbox); // Back to initial condition
click($checkRowCheckboxes);
assert.isFalse($checkAllCheckbox.prop('checked'));
});
});
});
describe('with name, multiple and checkAll options, multiple set to true and', function () {
withData({
'nothing else': [{}],
// https://github.com/yiisoft/yii2/pull/11729
'class option': [{'class': 'w0-check-row'}]
}, function (customOptions) {
it('should update data and "check all" functionality should work', function () {
$gridView = $('#w0').yiiGridView(settings);
var defaultOptions = {name: 'selection[]', multiple: true, checkAll: 'selection_all'};
var options = $.extend({}, defaultOptions, customOptions);
$gridView.yiiGridView('setSelectionColumn', options);
assert.equal($gridView.yiiGridView('data').selectionColumn, 'selection[]');
var $checkFirstRowCheckbox = $checkRowCheckboxes.filter('[value="1"]');
// Check all
click($checkAllCheckbox);
assert.lengthOf($checkRowCheckboxes.filter(':checked'), 3);
assert.isTrue($checkAllCheckbox.prop('checked'));
// Uncheck all
click($checkAllCheckbox);
assert.lengthOf($checkRowCheckboxes.filter(':checked'), 0);
assert.isFalse($checkAllCheckbox.prop('checked'));
// Check all manually
click($checkRowCheckboxes);
assert.lengthOf($checkRowCheckboxes.filter(':checked'), 3);
assert.isTrue($checkAllCheckbox.prop('checked'));
// Uncheck all manually
click($checkRowCheckboxes);
assert.lengthOf($checkRowCheckboxes.filter(':checked'), 0);
assert.isFalse($checkAllCheckbox.prop('checked'));
// Check first row
click($checkFirstRowCheckbox);
assert.isTrue($checkFirstRowCheckbox.prop('checked'));
assert.lengthOf($checkRowCheckboxes.filter(':checked'), 1);
assert.isFalse($checkAllCheckbox.prop('checked'));
// Then check all
click($checkAllCheckbox);
assert.lengthOf($checkRowCheckboxes.filter(':checked'), 3);
assert.isTrue($checkAllCheckbox.prop('checked'));
// Uncheck first row
click($checkFirstRowCheckbox);
assert.isFalse($checkFirstRowCheckbox.prop('checked'));
assert.lengthOf($checkRowCheckboxes.filter(':checked'), 2);
assert.isFalse($checkAllCheckbox.prop('checked'));
});
});
});
describe('with repeated calls', function () {
var jQueryPropStub;
before(function () {
jQueryPropStub = sinon.stub($, 'prop');
});
after(function () {
jQueryPropStub.restore();
});
it('should not duplicate event handler calls', function () {
$gridView = $('#w3').yiiGridView({
filterUrl: '/posts/index',
filterSelector: '#w3-filters input, #w3-filters select'
});
$gridView.yiiGridView('setSelectionColumn', {
name: 'selection[]',
multiple: true,
checkAll: 'selection_all'
});
// Change selectors to make sure event handlers are removed regardless of the selector
$gridView.yiiGridView('setSelectionColumn', {
name: 'selection2[]',
multiple: true,
checkAll: 'selection_all2'
});
$gridView.yiiGridView('setSelectionColumn', {
name: 'selection[]',
multiple: true,
checkAll: 'selection_all'
});
$gridView.yiiGridView('setSelectionColumn', {
'class': 'w3-check-row',
multiple: true,
checkAll: 'selection_all'
});
// Check first row ("prop" should be called once)
click($gridView.find('input[name="selection[]"][value="1"]'));
// Check all rows ("prop" should be called 2 times, 1 time for each row)
click($gridView.find('input[name="selection_all"]'));
assert.equal(jQueryPropStub.callCount, 3);
});
});
});
describe('getSelectedRows method', function () {
withData({
'selectionColumn not set, no rows selected': [undefined, [], false, []],
'selectionColumn not set, 1st and 2nd rows selected': [undefined, [1, 2], false, []],
'selectionColumn set, no rows selected': ['selection[]', [], false, []],
'selectionColumn set, 1st row selected': ['selection[]', [1], false, [1]],
'selectionColumn set, 1st and 2nd rows selected': ['selection[]', [1, 2], false, [1, 2]],
'selectionColumn set, all rows selected, "Check all" checkbox checked': [
'selection[]', [1, 2, 3], true, [1, 2, 3]
]
}, function (selectionColumn, selectedRows, checkAll, expectedSelectedRows) {
it('should return array with ids of selected rows', function () {
$gridView = $('#w0').yiiGridView(settings);
$gridView.yiiGridView('setSelectionColumn', {name: selectionColumn});
for (var i = 0; i < selectedRows.length; i++) {
$checkRowCheckboxes.filter('[value="' + selectedRows[i] + '"]').prop('checked', true);
}
if (checkAll) {
$checkAllCheckbox.prop('checked', true);
}
assert.deepEqual($gridView.yiiGridView('getSelectedRows'), expectedSelectedRows);
});
});
});
describe('destroy method', function () {
var jQuerySubmitStub;
var jQueryPropStub;
var beforeFilterSpy;
var afterFilterSpy;
beforeEach(function () {
jQuerySubmitStub = sinon.stub($.fn, 'submit');
jQueryPropStub = sinon.stub($, 'prop');
beforeFilterSpy = sinon.spy();
afterFilterSpy = sinon.spy();
});
afterEach(function () {
jQuerySubmitStub.restore();
jQueryPropStub.restore();
beforeFilterSpy.reset();
afterFilterSpy.reset();
});
it('should remove saved settings for destroyed element only and return initial jQuery object', function () {
$gridView = $('.grid-view').yiiGridView(commonSettings);
var $gridView1 = $('#w0');
var $gridView2 = $('#w1');
var destroyResult = $gridView1.yiiGridView('destroy');
assert.strictEqual(destroyResult, $gridView1);
assert.isUndefined($gridView1.yiiGridView('data'));
assert.deepEqual($gridView2.yiiGridView('data'), {settings: commonSettings});
});
it('should remove "beforeFilter" and "afterFilter" event handlers for destroyed element only', function () {
$gridView = $('.grid-view').yiiGridView(commonSettings)
.on('beforeFilter', beforeFilterSpy)
.on('afterFilter', afterFilterSpy);
var $gridView1 = $('#w0');
var $gridView2 = $('#w1');
$gridView1.yiiGridView('destroy');
assert.throws(function () {
$gridView1.yiiGridView('applyFilter');
}, "Cannot read property 'settings' of undefined");
$gridView1.yiiGridView(settings); // Reinitialize without "beforeFilter" and "afterFilter" event handlers
$gridView1.yiiGridView('applyFilter');
assert.isTrue(jQuerySubmitStub.calledOnce);
assert.isFalse(beforeFilterSpy.called);
assert.isFalse(afterFilterSpy.called);
$gridView2.yiiGridView('applyFilter');
assert.isTrue(jQuerySubmitStub.calledTwice);
assert.isTrue(beforeFilterSpy.calledOnce);
assert.isTrue(afterFilterSpy.calledOnce);
});
it('should remove "filter" event handler for destroyed element only', function () {
var $gridView1 = $('#w0');
var $gridView2 = $('#w1');
$gridView1.yiiGridView(settings);
$gridView2.yiiGridView({
filterUrl: '/posts/index',
filterSelector: '#w1-filters input, #w1-filters select'
});
$gridView2.yiiGridView('destroy');
pressEnter($gridView2.find('input[name="PostSearch[id]"]'));
assert.isFalse(jQuerySubmitStub.called);
pressEnter($textInput);
assert.isTrue(jQuerySubmitStub.calledOnce);
});
it('should remove "checkRow" and "checkAllRows" filter event handlers for destroyed element only', function () {
$gridView = $('.grid-view').yiiGridView(commonSettings);
var options = {name: 'selection[]', multiple: true, checkAll: 'selection_all'};
var $gridView1 = $('#w0');
var $gridView2 = $('#w1');
$gridView1.yiiGridView('setSelectionColumn', options);
$gridView2.yiiGridView('setSelectionColumn', options);
$gridView2.yiiGridView('destroy');
click($gridView2.find('input[name="selection_all"]'));
click($gridView2.find('input[name="selection[]"][value="1"]'));
assert.equal(jQueryPropStub.callCount, 0);
click($checkRowCheckboxes.filter('[value="1"]')); // Check first row ("prop" should be called once)
click($checkAllCheckbox); // Check all rows ("prop" should be called 3 times, 1 time for each row)
assert.equal(jQueryPropStub.callCount, 4);
});
});
describe('data method', function () {
it('should return saved settings', function () {
$gridView = $('#w0').yiiGridView(settings);
assert.deepEqual($gridView.yiiGridView('data'), {settings: settings});
});
});
describe('call of not existing method', function () {
it('should throw according error', function () {
$gridView = $('#w0').yiiGridView(settings);
assert.throws(function () {
$gridView.yiiGridView('foobar');
}, 'Method foobar does not exist in jQuery.yiiGridView');
});
});
});
Loading…
Cancel
Save