From 9247607184a3bf21317e699eea482cb539f7b277 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Bartosz=20Dziewo=C5=84ski?= Date: Mon, 15 Sep 2014 00:16:11 +0200 Subject: [PATCH] mediawiki.api.options: Add module for API action=options Implemented mw.Api#saveOptions to save user preferences. If necessary, the options will be saved using several parallel API requests. Only one promise is returned that resolves when all requests are complete. If a value of `null` is provided, the given option will be to reset to the default value. Any warnings returned by the API, including warnings about invalid option names or values, are currently ignored. This basically means that all requests will succeed (barring networks problems, internal server errors and such). Change-Id: Ia015898ca910923e00bc53f099b4e5631d6ad45c --- resources/Resources.php | 4 + .../mediawiki.api/mediawiki.api.options.js | 89 +++++++++++++++++++ tests/qunit/QUnitTestResources.php | 2 + .../mediawiki.api.options.test.js | 78 ++++++++++++++++ 4 files changed, 173 insertions(+) create mode 100644 resources/src/mediawiki.api/mediawiki.api.options.js create mode 100644 tests/qunit/suites/resources/mediawiki.api/mediawiki.api.options.test.js diff --git a/resources/Resources.php b/resources/Resources.php index b78fff9cd1..f88e587732 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -820,6 +820,10 @@ return array( 'scripts' => 'resources/src/mediawiki.api/mediawiki.api.login.js', 'dependencies' => 'mediawiki.api', ), + 'mediawiki.api.options' => array( + 'scripts' => 'resources/src/mediawiki.api/mediawiki.api.options.js', + 'dependencies' => 'mediawiki.api', + ), 'mediawiki.api.parse' => array( 'scripts' => 'resources/src/mediawiki.api/mediawiki.api.parse.js', 'dependencies' => 'mediawiki.api', diff --git a/resources/src/mediawiki.api/mediawiki.api.options.js b/resources/src/mediawiki.api/mediawiki.api.options.js new file mode 100644 index 0000000000..b839fbdc54 --- /dev/null +++ b/resources/src/mediawiki.api/mediawiki.api.options.js @@ -0,0 +1,89 @@ +/** + * @class mw.Api.plugin.options + */ +( function ( mw, $ ) { + + $.extend( mw.Api.prototype, { + + /** + * Asynchronously save the value of a single user option using the API. See #saveOptions. + * + * @param {string} name + * @param {string|null} value + * @return {jQuery.Promise} + */ + saveOption: function ( name, value ) { + var param = {}; + param[name] = value; + return this.saveOptions( param ); + }, + + /** + * Asynchronously save the values of user options using the API. + * + * If a value of `null` is provided, the given option will be reset to the default value. + * + * Any warnings returned by the API, including warnings about invalid option names or values, + * are ignored. However, do not rely on this behavior. + * + * If necessary, the options will be saved using several parallel API requests. Only one promise + * is always returned that will be resolved when all requests complete. + * + * @param {Object} options Options as a `{ name: value, … }` object + * @return {jQuery.Promise} + */ + saveOptions: function ( options ) { + var name, value, bundleable, + grouped = [], + deferreds = []; + + for ( name in options ) { + value = options[name] === null ? null : String( options[name] ); + + // Can we bundle this option, or does it need a separate request? + bundleable = + ( value === null || value.indexOf( '|' ) === -1 ) && + ( name.indexOf( '|' ) === -1 && name.indexOf( '=' ) === -1 ); + + if ( bundleable ) { + if ( value !== null ) { + grouped.push( name + '=' + value ); + } else { + // Omitting value resets the option + grouped.push( name ); + } + } else { + if ( value !== null ) { + deferreds.push( this.postWithToken( 'options', { + action: 'options', + optionname: name, + optionvalue: value + } ) ); + } else { + // Omitting value resets the option + deferreds.push( this.postWithToken( 'options', { + action: 'options', + optionname: name + } ) ); + } + } + } + + if ( grouped.length ) { + deferreds.push( this.postWithToken( 'options', { + action: 'options', + change: grouped.join( '|' ) + } ) ); + } + + return $.when.apply( $, deferreds ); + } + + } ); + + /** + * @class mw.Api + * @mixins mw.Api.plugin.options + */ + +}( mediaWiki, jQuery ) ); diff --git a/tests/qunit/QUnitTestResources.php b/tests/qunit/QUnitTestResources.php index 494727a1e1..84304135af 100644 --- a/tests/qunit/QUnitTestResources.php +++ b/tests/qunit/QUnitTestResources.php @@ -73,6 +73,7 @@ return array( 'tests/qunit/suites/resources/mediawiki/mediawiki.util.test.js', 'tests/qunit/suites/resources/mediawiki.api/mediawiki.api.test.js', 'tests/qunit/suites/resources/mediawiki.api/mediawiki.api.category.test.js', + 'tests/qunit/suites/resources/mediawiki.api/mediawiki.api.options.test.js', 'tests/qunit/suites/resources/mediawiki.api/mediawiki.api.parse.test.js', 'tests/qunit/suites/resources/mediawiki.api/mediawiki.api.watch.test.js', 'tests/qunit/suites/resources/mediawiki.special/mediawiki.special.recentchanges.test.js', @@ -99,6 +100,7 @@ return array( 'jquery.textSelection', 'mediawiki.api', 'mediawiki.api.category', + 'mediawiki.api.options', 'mediawiki.api.parse', 'mediawiki.api.watch', 'mediawiki.jqueryMsg', diff --git a/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.options.test.js b/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.options.test.js new file mode 100644 index 0000000000..c0a6585f30 --- /dev/null +++ b/tests/qunit/suites/resources/mediawiki.api/mediawiki.api.options.test.js @@ -0,0 +1,78 @@ +( function ( mw ) { + QUnit.module( 'mediawiki.api.options', QUnit.newMwEnvironment( { + setup: function () { + this.server = this.sandbox.useFakeServer(); + } + } ) ); + + QUnit.test( 'saveOption', function ( assert ) { + QUnit.expect( 2 ); + + var + api = new mw.Api(), + stub = this.sandbox.stub( mw.Api.prototype, 'saveOptions' ); + + api.saveOption( 'foo', 'bar' ); + + assert.ok( stub.calledOnce, '#saveOptions called once' ); + assert.deepEqual( stub.getCall( 0 ).args, [ { foo: 'bar' } ], '#saveOptions called correctly' ); + } ); + + QUnit.test( 'saveOptions', function ( assert ) { + QUnit.expect( 13 ); + + var api = new mw.Api(); + + // We need to respond to the request for token first, otherwise the other requests won't be sent + // until after the server.respond call, which confuses sinon terribly. This sucks a lot. + api.getToken( 'options' ); + this.server.respond( + /action=tokens.*&type=options/, + [ 200, { 'Content-Type': 'application/json' }, + '{ "tokens": { "optionstoken": "+\\\\" } }' ] + ); + + api.saveOptions( {} ).done( function () { + assert.ok( true, 'Request completed: empty case' ); + } ); + api.saveOptions( { foo: 'bar' } ).done( function () { + assert.ok( true, 'Request completed: simple' ); + } ); + api.saveOptions( { foo: 'bar', baz: 'quux' } ).done( function () { + assert.ok( true, 'Request completed: two options' ); + } ); + api.saveOptions( { foo: 'bar|quux', bar: 'a|b|c', baz: 'quux' } ).done( function () { + assert.ok( true, 'Request completed: not bundleable' ); + } ); + api.saveOptions( { foo: null } ).done( function () { + assert.ok( true, 'Request completed: reset an option' ); + } ); + api.saveOptions( { 'foo|bar=quux': null } ).done( function () { + assert.ok( true, 'Request completed: reset an option, not bundleable' ); + } ); + + // Requests are POST, match requestBody instead of url + this.server.respond( function ( request ) { + switch ( request.requestBody ) { + // simple + case 'action=options&format=json&change=foo%3Dbar&token=%2B%5C': + // two options + case 'action=options&format=json&change=foo%3Dbar%7Cbaz%3Dquux&token=%2B%5C': + // not bundleable + case 'action=options&format=json&optionname=foo&optionvalue=bar%7Cquux&token=%2B%5C': + case 'action=options&format=json&optionname=bar&optionvalue=a%7Cb%7Cc&token=%2B%5C': + case 'action=options&format=json&change=baz%3Dquux&token=%2B%5C': + // reset an option + case 'action=options&format=json&change=foo&token=%2B%5C': + // reset an option, not bundleable + case 'action=options&format=json&optionname=foo%7Cbar%3Dquux&token=%2B%5C': + assert.ok( true, 'Repond to ' + request.requestBody ); + request.respond( 200, { 'Content-Type': 'application/json' }, + '{ "options": "success" }' ); + break; + default: + assert.ok( false, 'Unexpected request:' + request.requestBody ); + } + } ); + } ); +}( mediaWiki ) ); -- 2.20.1