mirror of
				https://github.com/yiisoft/yii2.git
				synced 2025-10-31 10:39:59 +08:00 
			
		
		
		
	Merge branch 'master' into bugfix/20002-Superfluous_query_on_HEAD_request_in_serializer
This commit is contained in:
		| @ -76,8 +76,8 @@ | ||||
|         "ezyang/htmlpurifier": "^4.6", | ||||
|         "cebe/markdown": "~1.0.0 | ~1.1.0 | ~1.2.0", | ||||
|         "bower-asset/jquery": "3.7.*@stable | 3.6.*@stable | 3.5.*@stable | 3.4.*@stable | 3.3.*@stable | 3.2.*@stable | 3.1.*@stable | 2.2.*@stable | 2.1.*@stable | 1.11.*@stable | 1.12.*@stable", | ||||
|         "bower-asset/inputmask": "~3.2.2 | ~3.3.5 | ~5.0.8 ", | ||||
|         "bower-asset/punycode": "1.3.* | 2.2.*", | ||||
|         "bower-asset/inputmask": "^5.0.8 ", | ||||
|         "bower-asset/punycode": "^2.2", | ||||
|         "bower-asset/yii2-pjax": "~2.0.1", | ||||
|         "paragonie/random_compat": ">=1" | ||||
|     }, | ||||
|  | ||||
| @ -4,6 +4,8 @@ Yii Framework 2 Change Log | ||||
| 2.0.50 under development | ||||
| ------------------------ | ||||
|  | ||||
| - Bug #13920: Fixed erroneous validation for specific cases (tim-fischer-maschinensucher) | ||||
| - Bug #19927: Fixed `console\controllers\MessageController` when saving translations to database: fixed FK error when adding new string and language at the same time, checking/regenerating all missing messages and dropping messages for unused languages (atrandafir) | ||||
| - Enh #12743: Added new methods `BaseActiveRecord::loadRelations()` and `BaseActiveRecord::loadRelationsFor()` to eager load related models for existing primary model instances (PowerGamer1) | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -395,9 +395,11 @@ | ||||
|                         data: $form.serialize() + extData, | ||||
|                         dataType: data.settings.ajaxDataType, | ||||
|                         complete: function (jqXHR, textStatus) { | ||||
|                             currentAjaxRequest = null; | ||||
|                             $form.trigger(events.ajaxComplete, [jqXHR, textStatus]); | ||||
|                         }, | ||||
|                         beforeSend: function (jqXHR, settings) { | ||||
|                             currentAjaxRequest = jqXHR; | ||||
|                             $form.trigger(events.ajaxBeforeSend, [jqXHR, settings]); | ||||
|                         }, | ||||
|                         success: function (msgs) { | ||||
| @ -563,6 +565,9 @@ | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (currentAjaxRequest !== null) { | ||||
|             currentAjaxRequest.abort(); | ||||
|         } | ||||
|         if (data.settings.timer !== undefined) { | ||||
|             clearTimeout(data.settings.timer); | ||||
|         } | ||||
| @ -929,4 +934,7 @@ | ||||
|             $form.find(attribute.input).attr('aria-invalid', hasError ? 'true' : 'false'); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     var currentAjaxRequest = null; | ||||
|  | ||||
| })(window.jQuery); | ||||
|  | ||||
| @ -71,8 +71,8 @@ | ||||
|         "ezyang/htmlpurifier": "^4.6", | ||||
|         "cebe/markdown": "~1.0.0 | ~1.1.0 | ~1.2.0", | ||||
|         "bower-asset/jquery": "3.7.*@stable | 3.6.*@stable | 3.5.*@stable | 3.4.*@stable | 3.3.*@stable | 3.2.*@stable | 3.1.*@stable | 2.2.*@stable | 2.1.*@stable | 1.11.*@stable | 1.12.*@stable", | ||||
|         "bower-asset/inputmask": "~3.2.2 | ~3.3.5 | ~5.0.8 ", | ||||
|         "bower-asset/punycode": "1.3.* | 2.2.*", | ||||
|         "bower-asset/inputmask": "^5.0.8 ", | ||||
|         "bower-asset/punycode": "^2.2", | ||||
|         "bower-asset/yii2-pjax": "~2.0.1", | ||||
|         "paragonie/random_compat": ">=1" | ||||
|     }, | ||||
|  | ||||
| @ -354,16 +354,6 @@ EOD; | ||||
|             $currentMessages[$row['category']][$row['id']] = $row['message']; | ||||
|         } | ||||
|          | ||||
|         $currentLanguages = []; | ||||
|         $rows = (new Query())->select(['language'])->from($messageTable)->groupBy('language')->all($db); | ||||
|         foreach ($rows as $row) { | ||||
|             $currentLanguages[] = $row['language']; | ||||
|         } | ||||
|         $missingLanguages = []; | ||||
|         if (!empty($currentLanguages)) { | ||||
|             $missingLanguages = array_diff($languages, $currentLanguages); | ||||
|         } | ||||
|  | ||||
|         $new = []; | ||||
|         $obsolete = []; | ||||
|  | ||||
| @ -372,91 +362,132 @@ EOD; | ||||
|  | ||||
|             if (isset($currentMessages[$category])) { | ||||
|                 $new[$category] = array_diff($msgs, $currentMessages[$category]); | ||||
|                 // obsolete messages per category | ||||
|                 $obsolete += array_diff($currentMessages[$category], $msgs); | ||||
|             } else { | ||||
|                 $new[$category] = $msgs; | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         // obsolete categories | ||||
|         foreach (array_diff(array_keys($currentMessages), array_keys($messages)) as $category) { | ||||
|             $obsolete += $currentMessages[$category]; | ||||
|         } | ||||
|  | ||||
|         if (!$removeUnused) { | ||||
|             foreach ($obsolete as $pk => $msg) { | ||||
|                 // skip already marked unused | ||||
|                 if (strncmp($msg, '@@', 2) === 0 && substr($msg, -2) === '@@') { | ||||
|                     unset($obsolete[$pk]); | ||||
|                 } | ||||
|             } | ||||
|         }         | ||||
|          | ||||
|         $obsolete = array_keys($obsolete); | ||||
|         $this->stdout('Inserting new messages...'); | ||||
|         $savedFlag = false; | ||||
|         $insertCount = 0; | ||||
|  | ||||
|         foreach ($new as $category => $msgs) { | ||||
|             foreach ($msgs as $msg) { | ||||
|                 $savedFlag = true; | ||||
|                 $lastPk = $db->schema->insert($sourceMessageTable, ['category' => $category, 'message' => $msg]); | ||||
|                 foreach ($languages as $language) { | ||||
|                     $db->createCommand() | ||||
|                        ->insert($messageTable, ['id' => $lastPk['id'], 'language' => $language]) | ||||
|                        ->execute(); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (!empty($missingLanguages)) { | ||||
|             $updatedMessages = []; | ||||
|             $rows = (new Query())->select(['id', 'category', 'message'])->from($sourceMessageTable)->all($db); | ||||
|             foreach ($rows as $row) { | ||||
|                 $updatedMessages[$row['category']][$row['id']] = $row['message']; | ||||
|             } | ||||
|             foreach ($updatedMessages as $category => $msgs) { | ||||
|                 foreach ($msgs as $id => $msg) { | ||||
|                     $savedFlag = true; | ||||
|                     foreach ($missingLanguages as $language) { | ||||
|                         $db->createCommand() | ||||
|                             ->insert($messageTable, ['id' => $id, 'language' => $language]) | ||||
|                             ->execute(); | ||||
|                     } | ||||
|                 } | ||||
|                 $insertCount++; | ||||
|                 $db->schema->insert($sourceMessageTable, ['category' => $category, 'message' => $msg]); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         $this->stdout($savedFlag ? "saved.\n" : "Nothing to save.\n"); | ||||
|         $this->stdout($insertCount ? "{$insertCount} saved.\n" : "Nothing to save.\n"); | ||||
|          | ||||
|         $this->stdout($removeUnused ? 'Deleting obsoleted messages...' : 'Updating obsoleted messages...'); | ||||
|  | ||||
|         if (empty($obsolete)) { | ||||
|             $this->stdout("Nothing obsoleted...skipped.\n"); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if ($obsolete) { | ||||
|             if ($removeUnused) { | ||||
|             $db->createCommand() | ||||
|                ->delete($sourceMessageTable, ['in', 'id', $obsolete]) | ||||
|                 $affected = $db->createCommand() | ||||
|                    ->delete($sourceMessageTable, ['in', 'id', array_keys($obsolete)]) | ||||
|                    ->execute(); | ||||
|             $this->stdout("deleted.\n"); | ||||
|                 $this->stdout("{$affected} deleted.\n"); | ||||
|             } elseif ($markUnused) { | ||||
|                 $marked=0; | ||||
|                 $rows = (new Query()) | ||||
|                     ->select(['id', 'message']) | ||||
|                     ->from($sourceMessageTable) | ||||
|                 ->where(['in', 'id', $obsolete]) | ||||
|                     ->where(['in', 'id', array_keys($obsolete)]) | ||||
|                     ->all($db); | ||||
|      | ||||
|                 foreach ($rows as $row) { | ||||
|                     $marked++; | ||||
|                     $db->createCommand()->update( | ||||
|                         $sourceMessageTable, | ||||
|                         ['message' => '@@' . $row['message'] . '@@'], | ||||
|                         ['id' => $row['id']] | ||||
|                     )->execute(); | ||||
|                 } | ||||
|             $this->stdout("updated.\n"); | ||||
|                 $this->stdout("{$marked} updated.\n"); | ||||
|             } else { | ||||
|                 $this->stdout("kept untouched.\n"); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         // get fresh message id list | ||||
|         $freshMessagesIds = []; | ||||
|         $rows = (new Query())->select(['id'])->from($sourceMessageTable)->all($db); | ||||
|         foreach ($rows as $row) { | ||||
|             $freshMessagesIds[] = $row['id']; | ||||
|         } | ||||
|              | ||||
|         $this->stdout("Generating missing rows..."); | ||||
|         $generatedMissingRows = []; | ||||
|          | ||||
|         foreach ($languages as $language) { | ||||
|           $count = 0; | ||||
|            | ||||
|           // get list of ids of translations for this language | ||||
|           $msgRowsIds = []; | ||||
|           $msgRows = (new Query())->select(['id'])->from($messageTable)->where([ | ||||
|               'language'=>$language, | ||||
|           ])->all($db); | ||||
|           foreach ($msgRows as $row) { | ||||
|               $msgRowsIds[] = $row['id']; | ||||
|           } | ||||
|            | ||||
|           // insert missing | ||||
|           foreach ($freshMessagesIds as $id) { | ||||
|             if (!in_array($id, $msgRowsIds)) { | ||||
|               $db->createCommand() | ||||
|                  ->insert($messageTable, ['id' => $id, 'language' => $language]) | ||||
|                  ->execute(); | ||||
|               $count++; | ||||
|             } | ||||
|           } | ||||
|           if ($count) { | ||||
|             $generatedMissingRows[] = "{$count} for {$language}"; | ||||
|           } | ||||
|         } | ||||
|          | ||||
|         $this->stdout($generatedMissingRows ? implode(", ", $generatedMissingRows).".\n" : "Nothing to do.\n"); | ||||
|          | ||||
|         $this->stdout("Dropping unused languages..."); | ||||
|         $droppedLanguages=[]; | ||||
|          | ||||
|         $currentLanguages = []; | ||||
|         $rows = (new Query())->select(['language'])->from($messageTable)->groupBy('language')->all($db); | ||||
|         foreach ($rows as $row) { | ||||
|             $currentLanguages[] = $row['language']; | ||||
|         } | ||||
|          | ||||
|         foreach ($currentLanguages as $currentLanguage) { | ||||
|           if (!in_array($currentLanguage, $languages)) { | ||||
|             $deleted=$db->createCommand()->delete($messageTable, "language=:language", [ | ||||
|                 'language'=>$currentLanguage, | ||||
|             ])->execute(); | ||||
|             $droppedLanguages[] = "removed {$deleted} rows for $currentLanguage"; | ||||
|           } | ||||
|         } | ||||
|          | ||||
|         $this->stdout($droppedLanguages ? implode(", ", $droppedLanguages).".\n" : "Nothing to do.\n"); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Extracts messages from a file. | ||||
|      * | ||||
|  | ||||
| @ -21,7 +21,7 @@ class MaskedInputAsset extends AssetBundle | ||||
| { | ||||
|     public $sourcePath = '@bower/inputmask/dist'; | ||||
|     public $js = [ | ||||
|         'jquery.inputmask.bundle.js', | ||||
|         'jquery.inputmask.js', | ||||
|     ]; | ||||
|     public $depends = [ | ||||
|         'yii\web\YiiAsset', | ||||
|  | ||||
| @ -48,3 +48,15 @@ | ||||
|         <div class="help-block"></div> | ||||
|     </div> | ||||
| </form> | ||||
| <form id="w3"> | ||||
|     <div class="form-group field-test-text2 required"> | ||||
|         <label class="control-label" for="test-text2">Test text</label> | ||||
|         <input type="text" id="test-text2" class="form-control" name="Test[text2]" aria-required="true"> | ||||
|         <div class="help-block"></div> | ||||
|     </div> | ||||
|     <div class="form-group field-test-text3 required"> | ||||
|         <label class="control-label" for="test-text3">Test text</label> | ||||
|         <input type="text" id="test-text3" class="form-control" name="Test[text3]" aria-required="true"> | ||||
|         <div class="help-block"></div> | ||||
|     </div> | ||||
| </form> | ||||
|  | ||||
| @ -27,6 +27,21 @@ describe('yii.activeForm', function () { | ||||
|         var script = new vm.Script(code); | ||||
|         var context = new vm.createContext({window: window, document: window.document, yii: yii}); | ||||
|         script.runInContext(context); | ||||
|         /** This is a workaround for a jsdom issue, that prevents :hidden and :visible from working as expected. | ||||
|          * @see https://github.com/jsdom/jsdom/issues/1048 */ | ||||
|         context.window.Element.prototype.getClientRects = function () { | ||||
|             var node = this; | ||||
|             while(node) { | ||||
|                 if(node === document) { | ||||
|                     break; | ||||
|                 } | ||||
|                 if (!node.style || node.style.display === 'none' || node.style.visibility === 'hidden') { | ||||
|                     return []; | ||||
|                 } | ||||
|                 node = node.parentNode; | ||||
|             } | ||||
|             return [{width: 100, height: 100}]; | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     var activeFormHtml = fs.readFileSync('tests/js/data/yii.activeForm.html', 'utf-8'); | ||||
| @ -117,6 +132,60 @@ describe('yii.activeForm', function () { | ||||
|                 assert.isFalse($activeForm.data('yiiActiveForm').validated); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         describe('with ajax validation', function () { | ||||
|             describe('with rapid validation of multiple fields', function () { | ||||
|                 it('should cancel overlapping ajax requests and not display outdated validation results', function () { | ||||
|                     $activeForm = $('#w3'); | ||||
|                     $activeForm.yiiActiveForm([{ | ||||
|                         id: 'test-text2', | ||||
|                         input: '#test-text2', | ||||
|                         container: '.field-test-text2', | ||||
|                         enableAjaxValidation: true | ||||
|                     }, { | ||||
|                         id: 'test-text3', | ||||
|                         input: '#test-text3', | ||||
|                         container: '.field-test-text3', | ||||
|                         enableAjaxValidation: true | ||||
|                     }], { | ||||
|                         validationUrl: '' | ||||
|                     }); | ||||
|  | ||||
|                     let requests = []; | ||||
|                     function fakeAjax(object) { | ||||
|                         const request = { | ||||
|                             jqXHR: { | ||||
|                                 abort: function () { | ||||
|                                     request.aborted = true; | ||||
|                                 } | ||||
|                             }, | ||||
|                             aborted: false, | ||||
|                             respond: function (response) { | ||||
|                                 if (this.aborted) { | ||||
|                                     return; | ||||
|                                 } | ||||
|                                 object.success(response); | ||||
|                                 object.complete(this.jqXHR, ''); | ||||
|                             } | ||||
|                         }; | ||||
|                         requests.push(request); | ||||
|                         object.beforeSend(request.jqXHR, ''); | ||||
|                     } | ||||
|  | ||||
|                     const ajaxStub = sinon.stub($, 'ajax', fakeAjax); | ||||
|                     $activeForm.yiiActiveForm('validateAttribute', 'test-text2'); | ||||
|                     assert.isTrue(requests.length === 1); | ||||
|                     $activeForm.yiiActiveForm('validateAttribute', 'test-text3'); | ||||
|                     // When validateAttribute was called on text2, its value was valid. | ||||
|                     // The value of text3 wasn't. | ||||
|                     requests[0].respond({'test-text3': ['Field cannot be empty']}); | ||||
|                     // When validateAttribute was called on text3, its value was valid. | ||||
|                     requests[1].respond([]); | ||||
|                     assert.isTrue($activeForm.find('.field-test-text3').hasClass('has-success')); | ||||
|                     ajaxStub.restore(); | ||||
|                 }); | ||||
|             }); | ||||
|         }) | ||||
|     }); | ||||
|  | ||||
|     describe('resetForm method', function () { | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 xicond
					xicond