From b1e7c753ae3658bfdb8fbf3694ab234a123e90d6 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sun, 7 Sep 2014 12:40:50 -0400 Subject: [PATCH 01/22] Fixes #2702: added doc about customizing error response format [skip ci] --- docs/guide/rest-error-handling.md | 53 ++++++++++++++++++++++++++++++- framework/web/Response.php | 2 +- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/docs/guide/rest-error-handling.md b/docs/guide/rest-error-handling.md index 541323ed07..00de78c723 100644 --- a/docs/guide/rest-error-handling.md +++ b/docs/guide/rest-error-handling.md @@ -17,7 +17,6 @@ Transfer-Encoding: chunked Content-Type: application/json; charset=UTF-8 { - "type": "yii\\web\\NotFoundHttpException", "name": "Not Found Exception", "message": "The requested resource was not found.", "code": 0, @@ -42,3 +41,55 @@ The following list summarizes the HTTP status code that are used by the Yii REST * `422`: Data validation failed (in response to a `POST` request, for example). Please check the response body for detailed error messages. * `429`: Too many requests. The request was rejected due to rate limiting. * `500`: Internal server error. This could be caused by internal program errors. + + +## Customizing Error Response + +Sometimes you may want to customize the default error response format. For example, instead of relying on +using different HTTP statuses to indicate different errors, you would like to always use 200 as HTTP status +and enclose the actual HTTP status code as part of the JSON structure in the response, like shown in the following, + + +``` +HTTP/1.1 200 OK +Date: Sun, 02 Mar 2014 05:31:43 GMT +Server: Apache/2.2.26 (Unix) DAV/2 PHP/5.4.20 mod_ssl/2.2.26 OpenSSL/0.9.8y +Transfer-Encoding: chunked +Content-Type: application/json; charset=UTF-8 + +{ + "success": false, + "data": { + "name": "Not Found Exception", + "message": "The requested resource was not found.", + "code": 0, + "status": 404 + } +} +``` + +To achieve this goal, you can respond to the `beforeSend` event of the `response` component in the application configuration: + +```php +return [ + // ... + 'components' => [ + 'response' => [ + 'class' => 'yii\web\Response', + 'on beforeSend' => function ($event) { + $response = $event->sender; + if ($response->data !== null && !empty(Yii::$app->request->get['suppress_response_code'])) { + $response->data = [ + 'success' => $response->isSuccessful, + 'data' => $response->data, + ]; + $response->statusCode = 200; + } + }, + ], + ], +]; +``` + +The above code will reformat the response (for both successful and failed responses) as explained when +`suppress_response_code` is passed as a `GET` parameter. diff --git a/framework/web/Response.php b/framework/web/Response.php index 84337f4ccb..4e6ae97652 100644 --- a/framework/web/Response.php +++ b/framework/web/Response.php @@ -345,7 +345,7 @@ class Response extends \yii\base\Response $headers = $this->getHeaders(); foreach ($headers as $name => $values) { $name = str_replace(' ', '-', ucwords(str_replace('-', ' ', $name))); - // set replace for first occurance of header but false afterwards to allow multiple + // set replace for first occurrence of header but false afterwards to allow multiple $replace = true; foreach ($values as $value) { header("$name: $value", $replace); From 8ff4db3c02d07b5b26721d9ac56ad238fe98f68c Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sun, 7 Sep 2014 12:42:18 -0400 Subject: [PATCH 02/22] added anchor [skip ci] --- docs/guide/rest-error-handling.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/rest-error-handling.md b/docs/guide/rest-error-handling.md index 00de78c723..9449c1f1d1 100644 --- a/docs/guide/rest-error-handling.md +++ b/docs/guide/rest-error-handling.md @@ -43,7 +43,7 @@ The following list summarizes the HTTP status code that are used by the Yii REST * `500`: Internal server error. This could be caused by internal program errors. -## Customizing Error Response +## Customizing Error Response Sometimes you may want to customize the default error response format. For example, instead of relying on using different HTTP statuses to indicate different errors, you would like to always use 200 as HTTP status From 06a3f5ff8a8308c7c0e0c8c4abb4e3c834eecbd6 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Mon, 8 Sep 2014 00:24:40 +0400 Subject: [PATCH 03/22] Fixes #4947 --- docs/guide/structure-views.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/structure-views.md b/docs/guide/structure-views.md index 9bb50c59b4..459ab1b9c3 100644 --- a/docs/guide/structure-views.md +++ b/docs/guide/structure-views.md @@ -40,7 +40,7 @@ $this->title = 'Login'; Within a view, you can access `$this` which refers to the [[yii\web\View|view component]] managing and rendering this view template. -Besides `$this`, there may be other predefined variables in a view, such as `$form` and `$model` in the above +Besides `$this`, there may be other predefined variables in a view, such as `$model` in the above example. These variables represent the data that are *pushed* into the view by [controllers](structure-controllers.md) or other objects whose trigger the [view rendering](#rendering-views). From abb807bd9d3f00268e29cecdc81c878ebae1f843 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Mon, 8 Sep 2014 00:25:59 +0400 Subject: [PATCH 04/22] Fixes #4948. Thanks, @johan162 --- docs/guide/start-gii.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/guide/start-gii.md b/docs/guide/start-gii.md index bab6a8ec4e..2505e84fc7 100644 --- a/docs/guide/start-gii.md +++ b/docs/guide/start-gii.md @@ -43,6 +43,16 @@ Thanks to that line, your application is in development mode, and will have alre http://hostname/index.php?r=gii ``` +> Note: if you are accessing gii from an IP address other than localhost, access will be denied by default. +> To circumvent that default, add the allowed IP addresses to the configuration: +> +```php +'gii' => [ + 'class' => 'yii\gii\Module', + 'allowedIPs' => ['127.0.0.1', '::1', '192.168.0.*', '192.168.178.20'] // adjust this to your needs +], +``` + ![Gii](images/start-gii.png) From 92f7b3fb0690e2bb47b9931aac0cbfee1f34e820 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sun, 7 Sep 2014 18:27:08 -0400 Subject: [PATCH 05/22] minor doc adjustment [skip ci] --- docs/guide/start-gii.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guide/start-gii.md b/docs/guide/start-gii.md index 2505e84fc7..ba0596b386 100644 --- a/docs/guide/start-gii.md +++ b/docs/guide/start-gii.md @@ -43,8 +43,8 @@ Thanks to that line, your application is in development mode, and will have alre http://hostname/index.php?r=gii ``` -> Note: if you are accessing gii from an IP address other than localhost, access will be denied by default. -> To circumvent that default, add the allowed IP addresses to the configuration: +> Note: If you are accessing Gii from a machine other than localhost, the access will be denied by default +> for security purpose. You can configure Gii to add the allowed IP addresses as follows, > ```php 'gii' => [ From a5245ad048f7bc2946ac2e1e9abeffb1f19846e2 Mon Sep 17 00:00:00 2001 From: Ragazzo Date: Sun, 7 Sep 2014 21:10:08 +0400 Subject: [PATCH 06/22] added array fixture class --- framework/test/ArrayFixture.php | 76 +++++++++++++++++++ .../unit/framework/test/ArrayFixtureTest.php | 58 ++++++++++++++ .../framework/test/data/array_fixture.php | 16 ++++ 3 files changed, 150 insertions(+) create mode 100644 framework/test/ArrayFixture.php create mode 100644 tests/unit/framework/test/ArrayFixtureTest.php create mode 100644 tests/unit/framework/test/data/array_fixture.php diff --git a/framework/test/ArrayFixture.php b/framework/test/ArrayFixture.php new file mode 100644 index 0000000000..9525185b40 --- /dev/null +++ b/framework/test/ArrayFixture.php @@ -0,0 +1,76 @@ + + * @since 2.0 + */ +class ArrayFixture extends Fixture implements \IteratorAggregate, \ArrayAccess, \Countable +{ + use ArrayAccessTrait; + + /** + * @var array the data rows. Each array element represents one row of data (column name => column value). + */ + public $data = []; + /** + * @var string|boolean the file path or path alias of the data file that contains the fixture data + * to be returned by [[getData()]]. You can set this property to be false to prevent loading any data. + */ + public $dataFile; + + /** + * Loads the fixture. + * + * The default implementation simply stores the data returned by [[getData()]] in [[data]]. + * You should usually override this method by putting the data into the underlying database. + */ + public function load() + { + $this->data = $this->getData(); + } + + /** + * Returns the fixture data. + * + * The default implementation will try to return the fixture data by including the external file specified by [[dataFile]]. + * The file should return the data array that will be stored in [[data]] after inserting into the database. + * + * @return array the data to be put into the database + * @throws InvalidConfigException if the specified data file does not exist. + */ + protected function getData() + { + if ($this->dataFile === false || $this->dataFile === null) { + return []; + } + $dataFile = Yii::getAlias($this->dataFile); + if (is_file($dataFile)) { + return require($dataFile); + } else { + throw new InvalidConfigException("Fixture data file does not exist: {$this->dataFile}"); + } + } + + /** + * @inheritdoc + */ + public function unload() + { + parent::unload(); + $this->data = []; + } + +} diff --git a/tests/unit/framework/test/ArrayFixtureTest.php b/tests/unit/framework/test/ArrayFixtureTest.php new file mode 100644 index 0000000000..fcf361a287 --- /dev/null +++ b/tests/unit/framework/test/ArrayFixtureTest.php @@ -0,0 +1,58 @@ +_fixture = new ArrayFixture(); + } + + public function testLoadUnloadParticularFile() + { + $this->_fixture->dataFile = '@yiiunit/framework/test/data/array_fixture.php'; + $this->assertEmpty($this->_fixture->data, 'fixture data should be empty'); + + $this->_fixture->load(); + + $this->assertCount(2, $this->_fixture->data, 'fixture data should match needed total count'); + $this->assertEquals('customer1', $this->_fixture['customer1']['name'], 'first fixture data should match'); + $this->assertEquals('customer2@example.com', $this->_fixture['customer2']['email'], 'second fixture data should match'); + } + + public function testNothingToLoad() + { + $this->_fixture->dataFile = false; + $this->assertEmpty($this->_fixture->data, 'fixture data should be empty'); + + $this->_fixture->load(); + $this->assertEmpty($this->_fixture->data, 'fixture data should not be loaded'); + } + + /** + * @expectedException yii\base\InvalidParamException + */ + public function testWrongDataFileException() + { + $this->_fixture->dataFile = '@wrong/fixtures/data/path/alias'; + $this->_fixture->load(); + } + +} diff --git a/tests/unit/framework/test/data/array_fixture.php b/tests/unit/framework/test/data/array_fixture.php new file mode 100644 index 0000000000..c5ccd4b384 --- /dev/null +++ b/tests/unit/framework/test/data/array_fixture.php @@ -0,0 +1,16 @@ + [ + 'email' => 'customer1@example.com', + 'name' => 'customer1', + 'address' => 'address1', + 'status' => 1, + ], + 'customer2' => [ + 'email' => 'customer2@example.com', + 'name' => 'customer2', + 'address' => 'address2', + 'status' => 2, + ], +]; From 377cb09aa84af0730694e68d0d4ead28240d5b88 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sun, 7 Sep 2014 18:33:30 -0400 Subject: [PATCH 07/22] Fixes #4945: Added `yii\test\ArrayFixture` --- framework/CHANGELOG.md | 1 + framework/test/ArrayFixture.php | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 3913e6aa9e..e322267398 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -271,6 +271,7 @@ Yii Framework 2 Change Log - Chg: When an ID is found to be in both `Application::controllerMap` and `Application::modules`, the former will take precedence (qiangxue) - New #3911: Added `yii\behaviors\SluggableBehavior` that fills the specified model attribute with the transliterated and adjusted version to use in URLs (creocoder) - New #4193: Added `yii\filters\Cors` CORS filter to allow Cross Origin Resource Sharing (pgaultier) +- New #4945: Added `yii\test\ArrayFixture` (Ragazzo) - New: Added `yii\base\InvalidValueException` (qiangxue) - New: Added `yii\caching\ArrayCache` (cebe) diff --git a/framework/test/ArrayFixture.php b/framework/test/ArrayFixture.php index 9525185b40..3cb8141fe6 100644 --- a/framework/test/ArrayFixture.php +++ b/framework/test/ArrayFixture.php @@ -9,10 +9,10 @@ namespace yii\test; use Yii; use yii\base\ArrayAccessTrait; -use yii\base\InvalidParamException; +use yii\base\InvalidConfigException; /** - * Base array fixture class, that supports loading data from data files. + * ArrayFixture represents arbitrary fixture that can be loaded from PHP files. * * @author Mark Jebri * @since 2.0 From 09ae5fb595eb2ee1be97bf7ef83ffc9b3349ce3c Mon Sep 17 00:00:00 2001 From: Anton Andersen Date: Mon, 8 Sep 2014 13:52:11 +0400 Subject: [PATCH 08/22] Make unit tests cleanup a DB after finish --- extensions/codeception/TestCase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/codeception/TestCase.php b/extensions/codeception/TestCase.php index a810cb6e51..fa51d88de4 100644 --- a/extensions/codeception/TestCase.php +++ b/extensions/codeception/TestCase.php @@ -80,7 +80,6 @@ class TestCase extends Test { parent::setUp(); $this->mockApplication(); - $this->unloadFixtures(); $this->loadFixtures(); } @@ -90,6 +89,7 @@ class TestCase extends Test protected function tearDown() { $this->destroyApplication(); + $this->unloadFixtures(); parent::tearDown(); } From 383b9d32abf564848ae6201633f1fb8c7928c4bf Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Mon, 8 Sep 2014 12:39:28 +0200 Subject: [PATCH 09/22] skip fixture controller test on HHVM --- tests/unit/extensions/faker/FixtureControllerTest.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/unit/extensions/faker/FixtureControllerTest.php b/tests/unit/extensions/faker/FixtureControllerTest.php index 7586d635e3..67048443be 100644 --- a/tests/unit/extensions/faker/FixtureControllerTest.php +++ b/tests/unit/extensions/faker/FixtureControllerTest.php @@ -24,6 +24,11 @@ class FixtureControllerTest extends TestCase { parent::setUp(); + if (defined('HHVM_VERSION')) { + // https://github.com/facebook/hhvm/issues/1447 + $this->markTestSkipped('Can not test on HHVM because require is cached.'); + } + $this->mockApplication(); $this->_fixtureController = Yii::createObject([ From 77091123e647f906e96359c4511e643977334c22 Mon Sep 17 00:00:00 2001 From: Jani Mikkonen Date: Mon, 8 Sep 2014 14:01:14 +0300 Subject: [PATCH 10/22] Update finnish translation --- framework/messages/fi/yii.php | 186 ++++++++++++++++++---------------- 1 file changed, 98 insertions(+), 88 deletions(-) diff --git a/framework/messages/fi/yii.php b/framework/messages/fi/yii.php index 6c1bac0ef6..150b4ef06e 100644 --- a/framework/messages/fi/yii.php +++ b/framework/messages/fi/yii.php @@ -16,91 +16,101 @@ * * NOTE: this file must be saved in UTF-8 encoding. */ -return array ( - 'The requested view "{name}" was not found.' => 'Pyydettyä näkymää "{name}" ei löytynyt.', - '(not set)' => '(ei asetettu)', - 'An internal server error occurred.' => 'Sisäinen palvelinvirhe.', - 'Are you sure you want to delete this item?' => 'Haluatko varmasti poistaa tämän?', - 'Delete' => 'Poista', - 'Error' => 'Virhe', - 'File upload failed.' => 'Tiedoston lähetys epäonnistui.', - 'Home' => 'Koti', - 'Invalid data received for parameter "{param}".' => 'Parametri "{param}" vastaanotti virheellistä dataa.', - 'Login Required' => 'Kirjautuminen vaaditaan', - 'Missing required arguments: {params}' => 'Pakolliset argumentit puuttuu: {params}', - 'Missing required parameters: {params}' => 'Pakolliset parametrit puuttuu: {params}', - 'No' => 'Ei', - 'No help for unknown command "{command}".' => 'Ei ohjetta tuntemattomalle komennolle "{command}".', - 'No help for unknown sub-command "{command}".' => 'Ei ohjetta tuntemattomalle alikomennolle "{command}".', - 'No results found.' => 'Ei tuloksia.', - 'Only files with these MIME types are allowed: {mimeTypes}.' => 'Sallittuja ovat vain tiedostot, joiden MIME-tyyppi on: {mimeTypes}.', - 'Only files with these extensions are allowed: {extensions}.' => 'Sallittuja ovat vain tiedostot, joiden tiedostopääte on: {extensions}.', - 'Page not found.' => 'Sivua ei löytynyt.', - 'Please fix the following errors:' => 'Korjaa seuraavat virheet:', - 'Please upload a file.' => 'Lähetä tiedosto.', - 'Showing {begin, number}-{end, number} of {totalCount, number} {totalCount, plural, one{item} other{items}}.' => 'Näytetään {begin, number}-{end, number} kaikkiaan {totalCount, number} {totalCount, plural, one{tuloksesta} other{tuloksesta}}.', - 'The file "{file}" is not an image.' => 'Tiedosto "{file}" ei ole kuva.', - 'The file "{file}" is too big. Its size cannot exceed {limit, number} {limit, plural, one{byte} other{bytes}}.' => 'Tiedosto "{file}" on liian iso. Sen koko ei voi olla suurempi kuin {limit, number} {limit, plural, one{tavu} other{tavua}}.', - 'The file "{file}" is too small. Its size cannot be smaller than {limit, number} {limit, plural, one{byte} other{bytes}}.' => 'Tiedosto "{file}" on liian pieni. Sen koko ei voi olla pienempi kuin {limit, number} {limit, plural, one{tavu} other{tavua}}.', - 'The format of {attribute} is invalid.' => 'Attribuutin {attribute} formaatti on virheellinen.', - 'The image "{file}" is too large. The height cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'Kuva "{file}" on liian suuri. Korkeus ei voi olla suurempi kuin {limit, number} {limit, plural, one{pikseli} other{pikseliä}}.', - 'The image "{file}" is too large. The width cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'Kuva "{file}" on liian suuri. Leveys ei voi olla suurempi kuin {limit, number} {limit, plural, one{pikseli} other{pikseliä}}.', - 'The image "{file}" is too small. The height cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'Kuva "{file}" on liian pieni. Korkeus ei voi olla pienempi kuin {limit, number} {limit, plural, one{pikseli} other{pikseliä}}.', - 'The image "{file}" is too small. The width cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'Kuva "{file}" on liian pieni. Leveys ei voi olla pienempi kuin {limit, number} {limit, plural, one{pikseli} other{pikseliä}}.', - 'The verification code is incorrect.' => 'Vahvistuskoodi on virheellinen.', - 'Total {count, number} {count, plural, one{item} other{items}}.' => 'Yhteensä {count, number} {count, plural, one{tulos} other{tulosta}}.', - 'Unable to verify your data submission.' => 'Tietojen lähetystä ei voida varmistaa.', - 'Unknown command "{command}".' => 'Tuntematon komento "{command}".', - 'Unknown option: --{name}' => 'Tuntematon valinta: --{name}', - 'Update' => 'Päivitä', - 'View' => 'Näytä', - 'Yes' => 'Kyllä', - 'You are not allowed to perform this action.' => 'Sinulla ei ole tarvittavia oikeuksia toiminnon suorittamiseen.', - 'You can upload at most {limit, number} {limit, plural, one{file} other{files}}.' => 'Voit lähettää enintään {limit, number} {limit, plural, one{tiedoston} other{tiedostoa}}.', - 'in {delta, plural, =1{a day} other{# days}}' => '{delta, plural, =1{päivässä} other{# päivässä}}', - 'in {delta, plural, =1{a minute} other{# minutes}}' => '{delta, plural, =1{minuutissa} other{# minuutissa}}', - 'in {delta, plural, =1{a month} other{# months}}' => '{delta, plural, =1{kuukaudessa} other{# kuukaudessa}}', - 'in {delta, plural, =1{a second} other{# seconds}}' => '{delta, plural, =1{sekunnissa} other{# sekunnissa}}', - 'in {delta, plural, =1{a year} other{# years}}' => '{delta, plural, =1{vuodessa} other{# vuodessa}}', - 'in {delta, plural, =1{an hour} other{# hours}}' => '{delta, plural, =1{tunnissa} other{# tunnissa}}', - 'the input value' => 'syötetty arvo', - '{attribute} "{value}" has already been taken.' => '{attribute} "{value}" on jo käytössä.', - '{attribute} cannot be blank.' => '{attribute} ei voi olla tyhjä.', - '{attribute} is invalid.' => '{attribute} on virheellinen.', - '{attribute} is not a valid URL.' => '{attribute} on virheellinen URL.', - '{attribute} is not a valid email address.' => '{attribute} on virheellinen sähköpostiosoite.', - '{attribute} must be "{requiredValue}".' => '{attribute} täytyy olla "{requiredValue}".', - '{attribute} must be a number.' => '{attribute} täytyy olla luku.', - '{attribute} must be a string.' => '{attribute} täytyy olla merkkijono.', - '{attribute} must be an integer.' => '{attribute} täytyy olla kokonaisluku.', - '{attribute} must be either "{true}" or "{false}".' => '{attribute} täytyy olla joko {true} tai {false}.', - '{attribute} must be greater than "{compareValue}".' => '{attribute} täytyy olla suurempi kuin "{compareValue}".', - '{attribute} must be greater than or equal to "{compareValue}".' => '{attribute} täytyy olla suurempi tai yhtä suuri kuin "{compareValue}".', - '{attribute} must be less than "{compareValue}".' => '{attribute} täytyy olla pienempi kuin "{compareValue}".', - '{attribute} must be less than or equal to "{compareValue}".' => '{attribute} täytyy olla pienempi tai yhtä suuri kuin "{compareValue}".', - '{attribute} must be no greater than {max}.' => '{attribute} ei saa olla suurempi kuin "{max}".', - '{attribute} must be no less than {min}.' => '{attribute} ei saa olla pienempi kuin "{min}".', - '{attribute} must be repeated exactly.' => '{attribute} täytyy toistaa täsmälleen.', - '{attribute} must not be equal to "{compareValue}".' => '{attribute} ei saa olla yhtä suuri kuin "{compareValue}".', - '{attribute} should contain at least {min, number} {min, plural, one{character} other{characters}}.' => '{attribute} tulisi sisältää vähintään {min, number} {min, plural, one{merkki} other{merkkiä}}.', - '{attribute} should contain at most {max, number} {max, plural, one{character} other{characters}}.' => '{attribute} tulisi sisältää enintään {max, number} {max, plural, one{merkki} other{merkkiä}}.', - '{attribute} should contain {length, number} {length, plural, one{character} other{characters}}.' => '{attribute} tulisi sisältää {length, number} {length, plural, one{merkki} other{merkkiä}}.', - '{delta, plural, =1{a day} other{# days}} ago' => '{delta, plural, =1{päivä} other{# päivää}} sitten', - '{delta, plural, =1{a minute} other{# minutes}} ago' => '{delta, plural, =1{minuutti} other{# minuuttia}} sitten', - '{delta, plural, =1{a month} other{# months}} ago' => '{delta, plural, =1{kuukausi} other{# kuukautta}} sitten', - '{delta, plural, =1{a second} other{# seconds}} ago' => '{delta, plural, =1{sekunti} other{# sekuntia}} sitten', - '{delta, plural, =1{a year} other{# years}} ago' => '{delta, plural, =1{vuosi} other{# vuotta}} sitten', - '{delta, plural, =1{an hour} other{# hours}} ago' => '{delta, plural, =1{tunti} other{# tuntia}} sitten', - '{n, plural, =1{# byte} other{# bytes}}' => '{n, plural, =1{# tavu} other{# tavua}}', - '{n, plural, =1{# gigabyte} other{# gigabytes}}' => '{n, plural, =1{# gigatavu} other{# gigatavua}}', - '{n, plural, =1{# kilobyte} other{# kilobytes}}' => '{n, plural, =1{# kilotavu} other{# kilotavua}}', - '{n, plural, =1{# megabyte} other{# megabytes}}' => '{n, plural, =1{# megatavu} other{# megatavua}}', - '{n, plural, =1{# petabyte} other{# petabytes}}' => '{n, plural, =1{# petatavu} other{# petatavua}}', - '{n, plural, =1{# terabyte} other{# terabytes}}' => '{n, plural, =1{# teratavu} other{# teratavua}}', - '{n} B' => '{n} t', - '{n} GB' => '{n} Gt', - '{n} KB' => '{n} kt', - '{n} MB' => '{n} Mt', - '{n} PB' => '{n} Pt', - '{n} TB' => '{n} Tt', -); +return [ + '{n, plural, =1{# gibibyte} other{# gibibytes}}' => '{n, plural, =1{# gibitavu} other{# gibitavua}}', + '{n, plural, =1{# kibibyte} other{# kibibytes}}' => '{n, plural, =1{# kibitavu} other{# kibitavua}}', + '{n, plural, =1{# mebibyte} other{# mebibytes}}' => '{n, plural, =1{# mebitavu} other{# mebitavua}}', + '{n, plural, =1{# pebibyte} other{# pebibytes}}' => '{n, plural, =1{# pebitavu} other{# pebitavua}}', + '{n, plural, =1{# tebibyte} other{# tebibytes}}' => '{n, plural, =1{# tebitavu} other{# tebitavua}}', + '{n} GiB' => 'GiB', + '{n} KiB' => 'KiB', + '{n} MiB' => 'MiB', + '{n} PiB' => 'PiB', + '{n} TiB' => 'TiB', + '(not set)' => '(ei asetettu)', + 'An internal server error occurred.' => 'Sisäinen palvelinvirhe.', + 'Are you sure you want to delete this item?' => 'Haluatko varmasti poistaa tämän?', + 'Delete' => 'Poista', + 'Error' => 'Virhe', + 'File upload failed.' => 'Tiedoston lähetys epäonnistui.', + 'Home' => 'Koti', + 'Invalid data received for parameter "{param}".' => 'Parametri "{param}" vastaanotti virheellistä dataa.', + 'Login Required' => 'Kirjautuminen vaaditaan', + 'Missing required arguments: {params}' => 'Pakolliset argumentit puuttuu: {params}', + 'Missing required parameters: {params}' => 'Pakolliset parametrit puuttuu: {params}', + 'No' => 'Ei', + 'No help for unknown command "{command}".' => 'Ei ohjetta tuntemattomalle komennolle "{command}".', + 'No help for unknown sub-command "{command}".' => 'Ei ohjetta tuntemattomalle alikomennolle "{command}".', + 'No results found.' => 'Ei tuloksia.', + 'Only files with these MIME types are allowed: {mimeTypes}.' => 'Sallittuja ovat vain tiedostot, joiden MIME-tyyppi on: {mimeTypes}.', + 'Only files with these extensions are allowed: {extensions}.' => 'Sallittuja ovat vain tiedostot, joiden tiedostopääte on: {extensions}.', + 'Page not found.' => 'Sivua ei löytynyt.', + 'Please fix the following errors:' => 'Korjaa seuraavat virheet:', + 'Please upload a file.' => 'Lähetä tiedosto.', + 'Showing {begin, number}-{end, number} of {totalCount, number} {totalCount, plural, one{item} other{items}}.' => 'Näytetään {begin, number}-{end, number} kaikkiaan {totalCount, number} {totalCount, plural, one{tuloksesta} other{tuloksesta}}.', + 'The file "{file}" is not an image.' => 'Tiedosto "{file}" ei ole kuva.', + 'The file "{file}" is too big. Its size cannot exceed {limit, number} {limit, plural, one{byte} other{bytes}}.' => 'Tiedosto "{file}" on liian iso. Sen koko ei voi olla suurempi kuin {limit, number} {limit, plural, one{tavu} other{tavua}}.', + 'The file "{file}" is too small. Its size cannot be smaller than {limit, number} {limit, plural, one{byte} other{bytes}}.' => 'Tiedosto "{file}" on liian pieni. Sen koko ei voi olla pienempi kuin {limit, number} {limit, plural, one{tavu} other{tavua}}.', + 'The format of {attribute} is invalid.' => 'Attribuutin {attribute} formaatti on virheellinen.', + 'The image "{file}" is too large. The height cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'Kuva "{file}" on liian suuri. Korkeus ei voi olla suurempi kuin {limit, number} {limit, plural, one{pikseli} other{pikseliä}}.', + 'The image "{file}" is too large. The width cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'Kuva "{file}" on liian suuri. Leveys ei voi olla suurempi kuin {limit, number} {limit, plural, one{pikseli} other{pikseliä}}.', + 'The image "{file}" is too small. The height cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'Kuva "{file}" on liian pieni. Korkeus ei voi olla pienempi kuin {limit, number} {limit, plural, one{pikseli} other{pikseliä}}.', + 'The image "{file}" is too small. The width cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'Kuva "{file}" on liian pieni. Leveys ei voi olla pienempi kuin {limit, number} {limit, plural, one{pikseli} other{pikseliä}}.', + 'The requested view "{name}" was not found.' => 'Pyydettyä näkymää "{name}" ei löytynyt.', + 'The verification code is incorrect.' => 'Vahvistuskoodi on virheellinen.', + 'Total {count, number} {count, plural, one{item} other{items}}.' => 'Yhteensä {count, number} {count, plural, one{tulos} other{tulosta}}.', + 'Unable to verify your data submission.' => 'Tietojen lähetystä ei voida varmistaa.', + 'Unknown command "{command}".' => 'Tuntematon komento "{command}".', + 'Unknown option: --{name}' => 'Tuntematon valinta: --{name}', + 'Update' => 'Päivitä', + 'View' => 'Näytä', + 'Yes' => 'Kyllä', + 'You are not allowed to perform this action.' => 'Sinulla ei ole tarvittavia oikeuksia toiminnon suorittamiseen.', + 'You can upload at most {limit, number} {limit, plural, one{file} other{files}}.' => 'Voit lähettää enintään {limit, number} {limit, plural, one{tiedoston} other{tiedostoa}}.', + 'in {delta, plural, =1{a day} other{# days}}' => '{delta, plural, =1{päivässä} other{# päivässä}}', + 'in {delta, plural, =1{a minute} other{# minutes}}' => '{delta, plural, =1{minuutissa} other{# minuutissa}}', + 'in {delta, plural, =1{a month} other{# months}}' => '{delta, plural, =1{kuukaudessa} other{# kuukaudessa}}', + 'in {delta, plural, =1{a second} other{# seconds}}' => '{delta, plural, =1{sekunnissa} other{# sekunnissa}}', + 'in {delta, plural, =1{a year} other{# years}}' => '{delta, plural, =1{vuodessa} other{# vuodessa}}', + 'in {delta, plural, =1{an hour} other{# hours}}' => '{delta, plural, =1{tunnissa} other{# tunnissa}}', + 'the input value' => 'syötetty arvo', + '{attribute} "{value}" has already been taken.' => '{attribute} "{value}" on jo käytössä.', + '{attribute} cannot be blank.' => '{attribute} ei voi olla tyhjä.', + '{attribute} is invalid.' => '{attribute} on virheellinen.', + '{attribute} is not a valid URL.' => '{attribute} on virheellinen URL.', + '{attribute} is not a valid email address.' => '{attribute} on virheellinen sähköpostiosoite.', + '{attribute} must be "{requiredValue}".' => '{attribute} täytyy olla "{requiredValue}".', + '{attribute} must be a number.' => '{attribute} täytyy olla luku.', + '{attribute} must be a string.' => '{attribute} täytyy olla merkkijono.', + '{attribute} must be an integer.' => '{attribute} täytyy olla kokonaisluku.', + '{attribute} must be either "{true}" or "{false}".' => '{attribute} täytyy olla joko {true} tai {false}.', + '{attribute} must be greater than "{compareValue}".' => '{attribute} täytyy olla suurempi kuin "{compareValue}".', + '{attribute} must be greater than or equal to "{compareValue}".' => '{attribute} täytyy olla suurempi tai yhtä suuri kuin "{compareValue}".', + '{attribute} must be less than "{compareValue}".' => '{attribute} täytyy olla pienempi kuin "{compareValue}".', + '{attribute} must be less than or equal to "{compareValue}".' => '{attribute} täytyy olla pienempi tai yhtä suuri kuin "{compareValue}".', + '{attribute} must be no greater than {max}.' => '{attribute} ei saa olla suurempi kuin "{max}".', + '{attribute} must be no less than {min}.' => '{attribute} ei saa olla pienempi kuin "{min}".', + '{attribute} must be repeated exactly.' => '{attribute} täytyy toistaa täsmälleen.', + '{attribute} must not be equal to "{compareValue}".' => '{attribute} ei saa olla yhtä suuri kuin "{compareValue}".', + '{attribute} should contain at least {min, number} {min, plural, one{character} other{characters}}.' => '{attribute} tulisi sisältää vähintään {min, number} {min, plural, one{merkki} other{merkkiä}}.', + '{attribute} should contain at most {max, number} {max, plural, one{character} other{characters}}.' => '{attribute} tulisi sisältää enintään {max, number} {max, plural, one{merkki} other{merkkiä}}.', + '{attribute} should contain {length, number} {length, plural, one{character} other{characters}}.' => '{attribute} tulisi sisältää {length, number} {length, plural, one{merkki} other{merkkiä}}.', + '{delta, plural, =1{a day} other{# days}} ago' => '{delta, plural, =1{päivä} other{# päivää}} sitten', + '{delta, plural, =1{a minute} other{# minutes}} ago' => '{delta, plural, =1{minuutti} other{# minuuttia}} sitten', + '{delta, plural, =1{a month} other{# months}} ago' => '{delta, plural, =1{kuukausi} other{# kuukautta}} sitten', + '{delta, plural, =1{a second} other{# seconds}} ago' => '{delta, plural, =1{sekunti} other{# sekuntia}} sitten', + '{delta, plural, =1{a year} other{# years}} ago' => '{delta, plural, =1{vuosi} other{# vuotta}} sitten', + '{delta, plural, =1{an hour} other{# hours}} ago' => '{delta, plural, =1{tunti} other{# tuntia}} sitten', + '{n, plural, =1{# byte} other{# bytes}}' => '{n, plural, =1{# tavu} other{# tavua}}', + '{n, plural, =1{# gigabyte} other{# gigabytes}}' => '{n, plural, =1{# gigatavu} other{# gigatavua}}', + '{n, plural, =1{# kilobyte} other{# kilobytes}}' => '{n, plural, =1{# kilotavu} other{# kilotavua}}', + '{n, plural, =1{# megabyte} other{# megabytes}}' => '{n, plural, =1{# megatavu} other{# megatavua}}', + '{n, plural, =1{# petabyte} other{# petabytes}}' => '{n, plural, =1{# petatavu} other{# petatavua}}', + '{n, plural, =1{# terabyte} other{# terabytes}}' => '{n, plural, =1{# teratavu} other{# teratavua}}', + '{n} B' => '{n} t', + '{n} GB' => '{n} Gt', + '{n} KB' => '{n} kt', + '{n} MB' => '{n} Mt', + '{n} PB' => '{n} Pt', + '{n} TB' => '{n} Tt', +]; From 8e66325fa16ee0b18366e9d7343c05d9c87be009 Mon Sep 17 00:00:00 2001 From: Anton Andersen Date: Mon, 8 Sep 2014 15:04:42 +0400 Subject: [PATCH 11/22] Return a fixtures cleanup in case of a failing test --- extensions/codeception/TestCase.php | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/codeception/TestCase.php b/extensions/codeception/TestCase.php index fa51d88de4..222df448a0 100644 --- a/extensions/codeception/TestCase.php +++ b/extensions/codeception/TestCase.php @@ -80,6 +80,7 @@ class TestCase extends Test { parent::setUp(); $this->mockApplication(); + $this->unloadFixtures(); $this->loadFixtures(); } From 91e973ce7b32b4d9ea959b4508f4e2df961dd899 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Mon, 8 Sep 2014 13:20:30 +0200 Subject: [PATCH 12/22] note about validation rules order fixes #2268 --- docs/guide/input-validation.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/guide/input-validation.md b/docs/guide/input-validation.md index 3ff638e7f7..ef6494d8ec 100644 --- a/docs/guide/input-validation.md +++ b/docs/guide/input-validation.md @@ -97,6 +97,7 @@ When the `validate()` method is called, it does the following steps to perform v 2. Determine which rules should be applied by checking the current [[yii\base\Model::scenario|scenario]] against the rules declared in [[yii\base\Model::rules()]]. These rules are the active rules. 3. Use each active rule to validate each active attribute which is associated with the rule. + The validation rules are evaluated in the order they are listed. According to the above validation steps, an attribute will be validated if and only if it is an active attribute declared in `scenarios()` and is associated with one or multiple active rules From 0a95c49149bde1720bf02cbd70ca97a327cd6a6b Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 8 Sep 2014 08:01:53 -0400 Subject: [PATCH 13/22] Fixed test case. --- tests/unit/framework/test/ArrayFixtureTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/framework/test/ArrayFixtureTest.php b/tests/unit/framework/test/ArrayFixtureTest.php index fcf361a287..e8df112355 100644 --- a/tests/unit/framework/test/ArrayFixtureTest.php +++ b/tests/unit/framework/test/ArrayFixtureTest.php @@ -47,7 +47,7 @@ class ArrayFixtureTest extends TestCase } /** - * @expectedException yii\base\InvalidParamException + * @expectedException \yii\base\InvalidConfigException */ public function testWrongDataFileException() { From c4141f94f6755e9e73d26d1108d8966e6ee905ee Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Mon, 8 Sep 2014 08:19:43 -0400 Subject: [PATCH 14/22] Fixed test break. --- tests/unit/framework/test/ArrayFixtureTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/framework/test/ArrayFixtureTest.php b/tests/unit/framework/test/ArrayFixtureTest.php index e8df112355..ec53e4ba31 100644 --- a/tests/unit/framework/test/ArrayFixtureTest.php +++ b/tests/unit/framework/test/ArrayFixtureTest.php @@ -51,7 +51,7 @@ class ArrayFixtureTest extends TestCase */ public function testWrongDataFileException() { - $this->_fixture->dataFile = '@wrong/fixtures/data/path/alias'; + $this->_fixture->dataFile = 'wrong/fixtures/data/path/alias'; $this->_fixture->load(); } From 1bd738243c9c77aa90b9ed4112acc4314da084e3 Mon Sep 17 00:00:00 2001 From: Alexander Kochetov Date: Tue, 9 Sep 2014 04:18:53 +0400 Subject: [PATCH 15/22] Typo fixed inside \yii\rest\CreateAction --- framework/rest/CreateAction.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/rest/CreateAction.php b/framework/rest/CreateAction.php index 209482e482..73a06cab4c 100644 --- a/framework/rest/CreateAction.php +++ b/framework/rest/CreateAction.php @@ -24,7 +24,7 @@ class CreateAction extends Action */ public $scenario = Model::SCENARIO_DEFAULT; /** - * @var string the name of the view action. This property is need to create the URL when the mode is successfully created. + * @var string the name of the view action. This property is need to create the URL when the model is successfully created. */ public $viewAction = 'view'; From d90fc6ced58b00ac99ad6aba0437578762ae7768 Mon Sep 17 00:00:00 2001 From: Steve Date: Tue, 9 Sep 2014 11:25:35 +0300 Subject: [PATCH 16/22] Fix bug in Estonian translation --- framework/messages/et/yii.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/messages/et/yii.php b/framework/messages/et/yii.php index 152888473d..a29a03440e 100644 --- a/framework/messages/et/yii.php +++ b/framework/messages/et/yii.php @@ -85,7 +85,7 @@ return array ( '{attribute} should contain at least {min, number} {min, plural, one{character} other{characters}}.' => '{attribute} peab sisaldama vähemalt {min, number} {min, plural, one{märki} other{märki}}.', '{attribute} should contain at most {max, number} {max, plural, one{character} other{characters}}.' => '{attribute} tohib sisaldada maksimaalselt {max, number} {max, plural, one{märki} other{märki}}.', '{attribute} should contain {length, number} {length, plural, one{character} other{characters}}.' => '{attribute} peab sisaldama {length, number} {length, plural, one{märki} other{märki}}.', - '{delta, plural, =1{a day} other{# days}} ago' => 'delta, plural, =1{üks päev} other{# päeva}} tagasi', + '{delta, plural, =1{a day} other{# days}} ago' => '{delta, plural, =1{üks päev} other{# päeva}} tagasi', '{delta, plural, =1{a minute} other{# minutes}} ago' => '{delta, plural, =1{üks minut} other{# minutit}} tagasi', '{delta, plural, =1{a month} other{# months}} ago' => '{delta, plural, =1{kuu aega} other{# kuud}} tagasi', '{delta, plural, =1{a second} other{# seconds}} ago' => '{delta, plural, =1{üks sekund} other{# sekundit}} tagasi', From ed251d899383e70c3ac7b3fb14eda87964e6cdc5 Mon Sep 17 00:00:00 2001 From: Alexander Kochetov Date: Tue, 9 Sep 2014 23:32:00 +0400 Subject: [PATCH 17/22] Html::button() type is `button` by default --- framework/helpers/BaseHtml.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/framework/helpers/BaseHtml.php b/framework/helpers/BaseHtml.php index 4b1259073e..83972b2420 100644 --- a/framework/helpers/BaseHtml.php +++ b/framework/helpers/BaseHtml.php @@ -425,6 +425,9 @@ class BaseHtml */ public static function button($content = 'Button', $options = []) { + if (!isset($options['type'])) { + $options['type'] = 'button'; + } return static::tag('button', $content, $options); } @@ -1541,7 +1544,7 @@ class BaseHtml $groups = isset($tagOptions['groups']) ? $tagOptions['groups'] : []; unset($tagOptions['prompt'], $tagOptions['options'], $tagOptions['groups']); $options['encodeSpaces'] = ArrayHelper::getValue($options, 'encodeSpaces', $encodeSpaces); - + foreach ($items as $key => $value) { if (is_array($value)) { $groupAttrs = isset($groups[$key]) ? $groups[$key] : []; From 090e550a3dcc121d29f3deb0c9a64f74d047cf4c Mon Sep 17 00:00:00 2001 From: Alexander Kochetov Date: Wed, 10 Sep 2014 05:55:07 +0400 Subject: [PATCH 18/22] Rename `\yii\web\User` component param for consistency --- framework/web/User.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/web/User.php b/framework/web/User.php index 5d595d320d..981b8fa82d 100644 --- a/framework/web/User.php +++ b/framework/web/User.php @@ -131,7 +131,7 @@ class User extends Component * @var string the session variable name used to store the value of absolute expiration timestamp of the authenticated state. * This is used when [[absoluteAuthTimeout]] is set. */ - public $absoluteAuthTimeoutParam = '__absolute_expire'; + public $absoluteAuthTimeoutParam = '__absoluteExpire'; /** * @var string the session variable name used to store the value of [[returnUrl]]. */ From cc40fb5b13e1a780eb7c40c2b0ddec7ce5a7528b Mon Sep 17 00:00:00 2001 From: Thiago Talma Date: Tue, 9 Sep 2014 23:24:51 -0300 Subject: [PATCH 19/22] Fix brackets There were too many brackets. --- docs/guide/structure-controllers.md | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/docs/guide/structure-controllers.md b/docs/guide/structure-controllers.md index edcd319fd7..79737be6e5 100644 --- a/docs/guide/structure-controllers.md +++ b/docs/guide/structure-controllers.md @@ -161,15 +161,13 @@ You may configure [[yii\base\Application::controllerMap|controller map]] in the ```php [ 'controllerMap' => [ - [ - // declares "account" controller using a class name - 'account' => 'app\controllers\UserController', + // declares "account" controller using a class name + 'account' => 'app\controllers\UserController', - // declares "article" controller using a configuration array - 'article' => [ - 'class' => 'app\controllers\PostController', - 'enableCsrfValidation' => false, - ], + // declares "article" controller using a configuration array + 'article' => [ + 'class' => 'app\controllers\PostController', + 'enableCsrfValidation' => false, ], ], ] From f50f840a55ade3537745d3594c32ab32c3d2ddd1 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Tue, 9 Sep 2014 22:37:27 -0400 Subject: [PATCH 20/22] Fixes #4955: Replaced callbacks with events for `ActiveForm` --- framework/CHANGELOG.md | 3 + framework/UPGRADE.md | 15 + framework/assets/yii.activeForm.js | 404 ++++++++++++---------- framework/widgets/ActiveForm.php | 79 ----- tests/unit/framework/helpers/HtmlTest.php | 2 +- 5 files changed, 235 insertions(+), 268 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index e322267398..4a91767446 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -258,6 +258,9 @@ Yii Framework 2 Change Log - Chg #4591: `yii\helpers\Url::to()` will no longer prefix relative URLs with the base URL (qiangxue) - Chg #4595: `yii\widgets\LinkPager`'s `nextPageLabel`, `prevPageLabel`, `firstPageLabel`, `lastPageLabel` are now taking `false` instead of `null` for "no label" (samdark) - Chg #4911: Changed callback signature used in `yii\base\ArrayableTrait::fields()` from `function ($field, $model) {` to `function ($model, $field) {` (samdark) +- Chg #4955: Replaced callbacks with events for `ActiveForm` (qiangxue) + - Removed `beforeValidate()`, `beforeValidateAll()`, `afterValidate()`, `afterValidateAll()`, `ajaxBeforeSend()` and `ajaxComplete()` from `ActiveForm`. + - Added `beforeValidate`, `afterValidate`, `beforeSubmit`, `ajaxBeforeSend` and `ajaxComplete` events to `yii.activeForm`. - Chg: Replaced `clearAll()` and `clearAllAssignments()` in `yii\rbac\ManagerInterface` with `removeAll()`, `removeAllRoles()`, `removeAllPermissions()`, `removeAllRules()` and `removeAllAssignments()` (qiangxue) - Chg: Added `$user` as the first parameter of `yii\rbac\Rule::execute()` (qiangxue) - Chg: `yii\grid\DataColumn::getDataCellValue()` visibility is now `public` to allow accessing the value from a GridView directly (cebe) diff --git a/framework/UPGRADE.md b/framework/UPGRADE.md index 88389412e9..24309306df 100644 --- a/framework/UPGRADE.md +++ b/framework/UPGRADE.md @@ -213,3 +213,18 @@ new ones save the following code as `convert.php` that should be placed in the s * `Html::radio()`, `Html::checkbox()`, `Html::radioList()`, `Html::checkboxList()` no longer generate the container tag around each radio/checkbox when you specify labels for them. You should manually render such container tags, or set the `item` option for `Html::radioList()`, `Html::checkboxList()` to generate the container tags. + +* `beforeValidate()`, `beforeValidateAll()`, `afterValidate()`, `afterValidateAll()`, `ajaxBeforeSend()` and `ajaxComplete()` + are removed from `ActiveForm`. The same functionality is now achieved via JavaScript event mechanism. For example, + if you want to do something before performing validation on the client side, you can write the following + JavaScript code: + + ```js + $('#myform').on('beforeValidate', function (event, messages, deferreds, attribute) { + if (attribute === undefined) { + // the event is triggered when submitting the form + } elseif (attribute.id === 'something') { + // the event is triggered before validating "something" + } + }); + ``` diff --git a/framework/assets/yii.activeForm.js b/framework/assets/yii.activeForm.js index a7a4f9013e..8d1429ad75 100644 --- a/framework/assets/yii.activeForm.js +++ b/framework/assets/yii.activeForm.js @@ -22,6 +22,66 @@ } }; + var events = { + /** + * beforeValidate event is triggered before validating the whole form and each attribute. + * The signature of the event handler should be: + * function (event, messages, deferreds, attribute) + * where + * - event: an Event object. You can set event.isValid to be false to stop validating the form or attribute + * - messages: error messages. When attribute is undefined, this parameter is an associative array + * with keys being attribute IDs and values being error messages for the corresponding attributes. + * When attribute is given, this parameter is an array of the error messages for that attribute. + * - deferreds: an array of Deferred objects. You can use deferreds.add(callback) to add a new deferred validation. + * - attribute: an attribute object. Please refer to attributeDefaults for the structure. + * If this is undefined, it means the event is triggered before validating the whole form. + * Otherwise it means the event is triggered before validating the specified attribute. + */ + beforeValidate: 'beforeValidate', + /** + * afterValidate event is triggered after validating the whole form and each attribute. + * The signature of the event handler should be: + * function (event, messages, attribute) + * where + * - event: an Event object. + * - messages: error messages. When attribute is undefined, this parameter is an associative array + * with keys being attribute IDs and values being error messages for the corresponding attributes. + * When attribute is given, this parameter is an array of the error messages for that attribute. + * If the array length is greater than 0, it means the attribute has validation errors. + * - attribute: an attribute object. Please refer to attributeDefaults for the structure. + * If this is undefined, it means the event is triggered before validating the whole form. + * Otherwise it means the event is triggered before validating the specified attribute. + */ + afterValidate: 'afterValidate', + /** + * beforeSubmit event is triggered before submitting the form (after all validations pass). + * The signature of the event handler should be: + * function (event) + * where event is an Event object. + */ + beforeSubmit: 'beforeSubmit', + /** + * ajaxBeforeSend event is triggered before sending an AJAX request for AJAX-based validation. + * The signature of the event handler should be: + * function (event, jqXHR, settings) + * where + * - event: an Event object. + * - jqXHR: a jqXHR object + * - settings: the settings for the AJAX request + */ + ajaxBeforeSend: 'ajaxBeforeSend', + /** + * ajaxComplete event is triggered after completing an AJAX request for AJAX-based validation. + * The signature of the event handler should be: + * function (event, jqXHR, textStatus) + * where + * - event: an Event object. + * - jqXHR: a jqXHR object + * - settings: the status of the request ("success", "notmodified", "error", "timeout", "abort", or "parsererror"). + */ + ajaxComplete: 'ajaxComplete' + }; + // NOTE: If you change any of these defaults, make sure you update yii\widgets\ActiveForm::getClientOptions() as well var defaults = { // whether to encode the error summary @@ -41,28 +101,7 @@ // the type of data that you're expecting back from the server ajaxDataType: 'json', // the URL for performing AJAX-based validation. If not set, it will use the the form's action - validationUrl: undefined, - // a callback that is called before submitting the form. The signature of the callback should be: - // function ($form) { ...return false to cancel submission...} - beforeSubmit: undefined, - // a callback that is called before validating each attribute. The signature of the callback should be: - // function ($form, attribute, messages) { ...return false to cancel the validation...} - beforeValidate: undefined, - // a callback that is called before validation starts (This callback is only called when the form is submitted). This signature of the callback should be: - // function($form, data) { ...return false to cancel the validation...} - beforeValidateAll: undefined, - // a callback that is called after an attribute is validated. The signature of the callback should be: - // function ($form, attribute, messages) - afterValidate: undefined, - // a callback that is called after all validation has run (This callback is only called when the form is submitted). The signature of the callback should be: - // function ($form, data, messages) - afterValidateAll: undefined, - // a pre-request callback function on AJAX-based validation. The signature of the callback should be: - // function ($form, jqXHR, textStatus) - ajaxBeforeSend: undefined, - // a function to be called when the request finishes on AJAX-based validation. The signature of the callback should be: - // function ($form, jqXHR, textStatus) - ajaxComplete: undefined + validationUrl: undefined }; // NOTE: If you change any of these defaults, make sure you update yii\widgets\ActiveField::getClientOptions() as well @@ -151,7 +190,7 @@ var $form = $(this), attributes = $form.data('yiiActiveForm').attributes, index = -1, - attribute; + attribute = undefined; $.each(attributes, function (i) { if (attributes[i]['id'] == id) { index = i; @@ -168,7 +207,8 @@ // find an attribute config based on the specified attribute ID find: function (id) { - var attributes = $(this).data('yiiActiveForm').attributes, result; + var attributes = $(this).data('yiiActiveForm').attributes, + result = undefined; $.each(attributes, function (i) { if (attributes[i]['id'] == id) { result = attributes[i]; @@ -189,66 +229,120 @@ return this.data('yiiActiveForm'); }, + validate: function () { + var $form = $(this), + data = $form.data('yiiActiveForm'), + needAjaxValidation = false, + messages = {}, + deferreds = deferredArray(); + + if (data.submitting) { + var event = $.Event(events.beforeValidate, {'isValid': true}); + $form.trigger(event, [messages, deferreds]); + if (!event.isValid) { + data.submitting = false; + return; + } + } + + // client-side validation + $.each(data.attributes, function () { + // perform validation only if the form is being submitted or if an attribute is pending validation + if (data.submitting || this.status === 2 || this.status === 3) { + var msg = messages[this.id]; + if (msg === undefined) { + msg = []; + messages[this.id] = msg; + } + var event = $.Event(events.beforeValidate, {'isValid': true}); + $form.trigger(event, [msg, deferreds, this]); + if (event.isValid) { + if (this.validate) { + this.validate(this, getValue($form, this), msg, deferreds); + } + if (this.enableAjaxValidation) { + needAjaxValidation = true; + } + } + } + }); + + // ajax validation + $.when.apply(this, deferreds).always(function() { + // Remove empty message arrays + for (var i in messages) { + if (0 === messages[i].length) { + delete messages[i]; + } + } + if (needAjaxValidation && (!data.submitting || $.isEmptyObject(messages))) { + // Perform ajax validation when at least one input needs it. + // If the validation is triggered by form submission, ajax validation + // should be done only when all inputs pass client validation + var $button = data.submitObject, + extData = '&' + data.settings.ajaxParam + '=' + $form.prop('id'); + if ($button && $button.length && $button.prop('name')) { + extData += '&' + $button.prop('name') + '=' + $button.prop('value'); + } + $.ajax({ + url: data.settings.validationUrl, + type: $form.prop('method'), + data: $form.serialize() + extData, + dataType: data.settings.ajaxDataType, + complete: function (jqXHR, textStatus) { + $form.trigger(events.ajaxComplete, [jqXHR, textStatus]); + }, + beforeSend: function (jqXHR, settings) { + $form.trigger(events.ajaxBeforeSend, [jqXHR, settings]); + }, + success: function (msgs) { + if (msgs !== null && typeof msgs === 'object') { + $.each(data.attributes, function () { + if (!this.enableAjaxValidation) { + delete msgs[this.id]; + } + }); + updateInputs($form, $.extend(messages, msgs)); + } else { + updateInputs($form, messages); + } + }, + error: function () { + data.submitting = false; + } + }); + } else if (data.submitting) { + // delay callback so that the form can be submitted without problem + setTimeout(function () { + updateInputs($form, messages); + }, 200); + } else { + updateInputs($form, messages); + } + }); + }, + submitForm: function () { var $form = $(this), data = $form.data('yiiActiveForm'); - if (data.validated) { - if (data.settings.beforeSubmit !== undefined) { - if (data.settings.beforeSubmit($form) == false) { - data.validated = false; - data.submitting = false; - return false; - } - } - // continue submitting the form since validation passes - return true; - } - if (data.settings.timer !== undefined) { - clearTimeout(data.settings.timer); - } - data.submitting = true; - - if (data.settings.beforeValidateAll && !data.settings.beforeValidateAll($form, data)) { - data.submitting = false; + if (data.validated) { + var event = $.Event(events.beforeSubmit, {'isValid': true}); + $form.trigger(event, [$form]); + if (!event.isValid) { + data.validated = false; + data.submitting = false; + return false; + } + return true; // continue submitting the form since validation passes + } else { + if (data.settings.timer !== undefined) { + clearTimeout(data.settings.timer); + } + data.submitting = true; + methods.validate.call($form); return false; } - validate($form, function (messages) { - var errors = []; - $.each(data.attributes, function () { - if (updateInput($form, this, messages)) { - errors.push(this.input); - } - }); - - if (data.settings.afterValidateAll) { - data.settings.afterValidateAll($form, data, messages); - } - - updateSummary($form, messages); - if (errors.length) { - var top = $form.find(errors.join(',')).first().offset().top; - var wtop = $(window).scrollTop(); - if (top < wtop || top > wtop + $(window).height) { - $(window).scrollTop(top); - } - } else { - data.validated = true; - var $button = data.submitObject || $form.find(':submit:first'); - // TODO: if the submission is caused by "change" event, it will not work - if ($button.length) { - $button.click(); - } else { - // no submit button in the form - $form.submit(); - } - return; - } - data.submitting = false; - }, function () { - data.submitting = false; - }); - return false; }, resetForm: function () { @@ -275,31 +369,6 @@ } }; - var watchAttributes = function ($form, attributes) { - $.each(attributes, function (i, attribute) { - var $input = findInput($form, attribute); - if (attribute.validateOnChange) { - $input.on('change.yiiActiveForm',function () { - validateAttribute($form, attribute, false); - }); - } - if (attribute.validateOnBlur) { - $input.on('blur.yiiActiveForm', function () { - if (attribute.status == 0 || attribute.status == 1) { - validateAttribute($form, attribute, !attribute.status); - } - }); - } - if (attribute.validateOnType) { - $input.on('keyup.yiiActiveForm', function () { - if (attribute.value !== getValue($form, attribute)) { - validateAttribute($form, attribute, false); - } - }); - } - }); - }; - var watchAttribute = function ($form, attribute) { var $input = findInput($form, attribute); if (attribute.validateOnChange) { @@ -356,14 +425,7 @@ $form.find(this.container).addClass(data.settings.validatingCssClass); } }); - validate($form, function (messages) { - var hasError = false; - $.each(data.attributes, function () { - if (this.status === 2 || this.status === 3) { - hasError = updateInput($form, this, messages) || hasError; - } - }); - }); + methods.validate.call($form); }, data.settings.validationDelay); }; @@ -379,88 +441,52 @@ }; return array; }; - + /** - * Performs validation. - * @param $form jQuery the jquery representation of the form - * @param successCallback function the function to be invoked if the validation completes - * @param errorCallback function the function to be invoked if the ajax validation request fails + * Updates the error messages and the input containers for all applicable attributes + * @param $form the form jQuery object + * @param messages array the validation error messages */ - var validate = function ($form, successCallback, errorCallback) { - var data = $form.data('yiiActiveForm'), - needAjaxValidation = false, - messages = {}, - deferreds = deferredArray(); + var updateInputs = function ($form, messages) { + var data = $form.data('yiiActiveForm'); - $.each(data.attributes, function () { - if (data.submitting || this.status === 2 || this.status === 3) { - var msg = []; - messages[this.id] = msg; - if (!data.settings.beforeValidate || data.settings.beforeValidate($form, this, msg)) { - if (this.validate) { - this.validate(this, getValue($form, this), msg, deferreds); - } - if (this.enableAjaxValidation) { - needAjaxValidation = true; - } + if (data.submitting) { + var errorInputs = []; + $.each(data.attributes, function () { + if (updateInput($form, this, messages)) { + errorInputs.push(this.input); } - } - }); + }); - $.when.apply(this, deferreds).always(function() { - //Remove empty message arrays - for (var i in messages) { - if (0 === messages[i].length) { - delete messages[i]; + $form.trigger(events.afterValidate, [messages]); + + updateSummary($form, messages); + + if (errorInputs.length) { + var top = $form.find(errorInputs.join(',')).first().offset().top; + var wtop = $(window).scrollTop(); + if (top < wtop || top > wtop + $(window).height) { + $(window).scrollTop(top); } - } - if (needAjaxValidation && (!data.submitting || $.isEmptyObject(messages))) { - // Perform ajax validation when at least one input needs it. - // If the validation is triggered by form submission, ajax validation - // should be done only when all inputs pass client validation - var $button = data.submitObject, - extData = '&' + data.settings.ajaxParam + '=' + $form.prop('id'); - if ($button && $button.length && $button.prop('name')) { - extData += '&' + $button.prop('name') + '=' + $button.prop('value'); - } - $.ajax({ - url: data.settings.validationUrl, - type: $form.prop('method'), - data: $form.serialize() + extData, - dataType: data.settings.ajaxDataType, - complete: function (jqXHR, textStatus) { - if (data.settings.ajaxComplete) { - data.settings.ajaxComplete($form, jqXHR, textStatus); - } - }, - beforeSend: function (jqXHR, textStatus) { - if (data.settings.ajaxBeforeSend) { - data.settings.ajaxBeforeSend($form, jqXHR, textStatus); - } - }, - success: function (msgs) { - if (msgs !== null && typeof msgs === 'object') { - $.each(data.attributes, function () { - if (!this.enableAjaxValidation) { - delete msgs[this.id]; - } - }); - successCallback($.extend({}, messages, msgs)); - } else { - successCallback(messages); - } - }, - error: errorCallback - }); - } else if (data.submitting) { - // delay callback so that the form can be submitted without problem - setTimeout(function () { - successCallback(messages); - }, 200); + data.submitting = false; } else { - successCallback(messages); + data.validated = true; + var $button = data.submitObject || $form.find(':submit:first'); + // TODO: if the submission is caused by "change" event, it will not work + if ($button.length) { + $button.click(); + } else { + // no submit button in the form + $form.submit(); + } } - }); + } else { + $.each(data.attributes, function () { + if (this.status === 2 || this.status === 3) { + updateInput($form, this, messages); + } + }); + } }; /** @@ -475,12 +501,14 @@ $input = findInput($form, attribute), hasError = false; - if (data.settings.afterValidate) { - data.settings.afterValidate($form, attribute, messages); + if (!$.isArray(messages[attribute.id])) { + messages[attribute.id] = []; } + $form.trigger(events.afterValidate, [messages[attribute.id], attribute]); + attribute.status = 1; if ($input.length) { - hasError = messages && $.isArray(messages[attribute.id]) && messages[attribute.id].length; + hasError = messages[attribute.id].length > 0; var $container = $form.find(attribute.container); var $error = $container.find(attribute.error); if (hasError) { diff --git a/framework/widgets/ActiveForm.php b/framework/widgets/ActiveForm.php index e9526655be..a4cbc3561c 100644 --- a/framework/widgets/ActiveForm.php +++ b/framework/widgets/ActiveForm.php @@ -15,7 +15,6 @@ use yii\helpers\ArrayHelper; use yii\helpers\Url; use yii\helpers\Html; use yii\helpers\Json; -use yii\web\JsExpression; /** * ActiveForm is a widget that builds an interactive HTML form for one or multiple data models. @@ -145,79 +144,6 @@ class ActiveForm extends Widget * @var string the type of data that you're expecting back from the server. */ public $ajaxDataType = 'json'; - /** - * @var string|JsExpression a JS callback that will be called when the form is being submitted. - * The signature of the callback should be: - * - * ~~~ - * function ($form) { - * ...return false to cancel submission... - * } - * ~~~ - */ - public $beforeSubmit; - /** - * @var string|JsExpression a JS callback that is called before validating an attribute. - * The signature of the callback should be: - * - * ~~~ - * function ($form, attribute, messages) { - * ...return false to cancel the validation... - * } - * ~~~ - */ - public $beforeValidate; - /** - * @var string|JsExpression a JS callback that is called before any validation has run (Only called when the form is submitted). - * The signature of the callback should be: - * - * ~~~ - * function ($form, data) { - * ...return false to cancel the validation... - * } - * ~~~ - */ - public $beforeValidateAll; - /** - * @var string|JsExpression a JS callback that is called after validating an attribute. - * The signature of the callback should be: - * - * ~~~ - * function ($form, attribute, messages) { - * } - * ~~~ - */ - public $afterValidate; - /** - * @var string|JsExpression a JS callback that is called after all validation has run (Only called when the form is submitted). - * The signature of the callback should be: - * - * ~~~ - * function ($form, data, messages) { - * } - * ~~~ - */ - public $afterValidateAll; - /** - * @var string|JsExpression a JS pre-request callback function on AJAX-based validation. - * The signature of the callback should be: - * - * ~~~ - * function ($form, jqXHR, textStatus) { - * } - * ~~~ - */ - public $ajaxBeforeSend; - /** - * @var string|JsExpression a JS callback to be called when the request finishes on AJAX-based validation. - * The signature of the callback should be: - * - * ~~~ - * function ($form, jqXHR, textStatus) { - * } - * ~~~ - */ - public $ajaxComplete; /** * @var array the client validation options for individual attributes. Each element of the array * represents the validation options for a particular attribute. @@ -282,11 +208,6 @@ class ActiveForm extends Widget if ($this->validationUrl !== null) { $options['validationUrl'] = Url::to($this->validationUrl); } - foreach (['beforeSubmit', 'beforeValidate', 'beforeValidateAll', 'afterValidate', 'afterValidateAll', 'ajaxBeforeSend', 'ajaxComplete'] as $name) { - if (($value = $this->$name) !== null) { - $options[$name] = $value instanceof JsExpression ? $value : new JsExpression($value); - } - } // only get the options that are different from the default ones (set in yii.activeForm.js) return array_diff_assoc($options, [ diff --git a/tests/unit/framework/helpers/HtmlTest.php b/tests/unit/framework/helpers/HtmlTest.php index aa0c37f17c..0614053f05 100644 --- a/tests/unit/framework/helpers/HtmlTest.php +++ b/tests/unit/framework/helpers/HtmlTest.php @@ -140,7 +140,7 @@ class HtmlTest extends TestCase public function testButton() { - $this->assertEquals('', Html::button()); + $this->assertEquals('', Html::button()); $this->assertEquals('', Html::button('content<>', ['name' => 'test', 'value' => 'value'])); $this->assertEquals('', Html::button('content<>', ['type' => 'submit', 'name' => 'test', 'value' => 'value', 'class' => "t"])); } From 00e4d63e57cde4dc3b679e9e603547700bf4af19 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Tue, 9 Sep 2014 22:51:26 -0400 Subject: [PATCH 21/22] Fixed test break. --- tests/unit/framework/helpers/HtmlTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/framework/helpers/HtmlTest.php b/tests/unit/framework/helpers/HtmlTest.php index 0614053f05..170ca9419a 100644 --- a/tests/unit/framework/helpers/HtmlTest.php +++ b/tests/unit/framework/helpers/HtmlTest.php @@ -141,7 +141,7 @@ class HtmlTest extends TestCase public function testButton() { $this->assertEquals('', Html::button()); - $this->assertEquals('', Html::button('content<>', ['name' => 'test', 'value' => 'value'])); + $this->assertEquals('', Html::button('content<>', ['name' => 'test', 'value' => 'value'])); $this->assertEquals('', Html::button('content<>', ['type' => 'submit', 'name' => 'test', 'value' => 'value', 'class' => "t"])); } From 3847630994a068d89a293ae5498bc98e7fc54e20 Mon Sep 17 00:00:00 2001 From: Stephen Seliuk Date: Wed, 10 Sep 2014 13:10:19 +0300 Subject: [PATCH 22/22] Fixes #4971: Fixed hardcoded table names in `viaTable` expression in model generator --- extensions/gii/CHANGELOG.md | 1 + extensions/gii/generators/model/Generator.php | 4 ++-- extensions/gii/generators/model/default/model.php | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/extensions/gii/CHANGELOG.md b/extensions/gii/CHANGELOG.md index e70314bf36..e7f062bdfe 100644 --- a/extensions/gii/CHANGELOG.md +++ b/extensions/gii/CHANGELOG.md @@ -9,6 +9,7 @@ Yii Framework 2 gii extension Change Log - Bug #3265: Fixed incorrect controller class name validation (suralc) - Bug #3693: Fixed broken Gii preview when a file is unchanged (cebe) - Bug #4410: Fixed Gii to preserve database column order in generated _form.php (kmindi) +- Bug #4971: Fixed hardcoded table names in `viaTable` expression in model generator (stepanselyuk) - Enh #2018: Search model is not required anymore in CRUD generator (johonunu) - Enh #3088: The gii module will manage their own URL rules now (qiangxue) - Enh #3222: Added `useTablePrefix` option to the model generator for Gii (horizons2) diff --git a/extensions/gii/generators/model/Generator.php b/extensions/gii/generators/model/Generator.php index 8a29795d77..ffbac2539a 100644 --- a/extensions/gii/generators/model/Generator.php +++ b/extensions/gii/generators/model/Generator.php @@ -354,7 +354,7 @@ class Generator extends \yii\gii\Generator $viaLink = $this->generateRelationLink([$table->primaryKey[0] => $fks[$table->primaryKey[0]][1]]); $relationName = $this->generateRelationName($relations, $className0, $db->getTableSchema($table0), $table->primaryKey[1], true); $relations[$className0][$relationName] = [ - "return \$this->hasMany($className1::className(), $link)->viaTable('{$table->name}', $viaLink);", + "return \$this->hasMany($className1::className(), $link)->viaTable('{" . $this->generateTableName($table->name) . "}', $viaLink);", $className1, true, ]; @@ -363,7 +363,7 @@ class Generator extends \yii\gii\Generator $viaLink = $this->generateRelationLink([$table->primaryKey[1] => $fks[$table->primaryKey[1]][1]]); $relationName = $this->generateRelationName($relations, $className1, $db->getTableSchema($table1), $table->primaryKey[0], true); $relations[$className1][$relationName] = [ - "return \$this->hasMany($className0::className(), $link)->viaTable('{$table->name}', $viaLink);", + "return \$this->hasMany($className0::className(), $link)->viaTable('{" . $this->generateTableName($table->name) . "}', $viaLink);", $className0, true, ]; diff --git a/extensions/gii/generators/model/default/model.php b/extensions/gii/generators/model/default/model.php index 67d9e8ace8..cd5a2fc0fd 100644 --- a/extensions/gii/generators/model/default/model.php +++ b/extensions/gii/generators/model/default/model.php @@ -20,7 +20,7 @@ namespace ns ?>; use Yii; /** - * This is the model class for table "". + * This is the model class for table "generateTableName($tableName) ?>". * columns as $column): ?> * @property phpType} \${$column->name}\n" ?>