mediawiki.api.options: Add module for API action=options
authorBartosz Dziewoński <matma.rex@gmail.com>
Sun, 14 Sep 2014 22:16:11 +0000 (00:16 +0200)
committerTimo Tijhof <krinklemail@gmail.com>
Wed, 18 Mar 2015 19:03:16 +0000 (19:03 +0000)
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
resources/src/mediawiki.api/mediawiki.api.options.js [new file with mode: 0644]
tests/qunit/QUnitTestResources.php
tests/qunit/suites/resources/mediawiki.api/mediawiki.api.options.test.js [new file with mode: 0644]

index b78fff9..f88e587 100644 (file)
@@ -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 (file)
index 0000000..b839fbd
--- /dev/null
@@ -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 ) );
index 494727a..8430413 100644 (file)
@@ -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 (file)
index 0000000..c0a6585
--- /dev/null
@@ -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 ) );