Mattflaschen has uploaded a new change for review. https://gerrit.wikimedia.org/r/200801
Change subject: Add pluggable talk page poster and use it for mediawiki.feedback ...................................................................... Add pluggable talk page poster and use it for mediawiki.feedback The core implementation will only support wikitext. Flow will add its own implementation, and it can be used for any talk page system identifiable by content model. Bug: T91805 Change-Id: Ic69acafb24aa737536fe3a074e1958690732f0a7 --- M jsduck.json M languages/i18n/en.json M languages/i18n/qqq.json M maintenance/jsduck/categories.json M resources/Resources.php A resources/src/mediawiki.messagePoster/mediawiki.messagePoster.MessagePoster.js A resources/src/mediawiki.messagePoster/mediawiki.messagePoster.WikitextMessagePoster.js A resources/src/mediawiki.messagePoster/mediawiki.messagePoster.messagePosterFactory.js M resources/src/mediawiki/mediawiki.feedback.js M tests/qunit/QUnitTestResources.php A tests/qunit/suites/resources/mediawiki/mediawiki.messagePosterFactory.test.js 11 files changed, 313 insertions(+), 37 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/core refs/changes/01/200801/1 diff --git a/jsduck.json b/jsduck.json index 53c6913..2b0c8fb 100644 --- a/jsduck.json +++ b/jsduck.json @@ -15,6 +15,7 @@ "resources/src/mediawiki.action", "resources/src/mediawiki.api", "resources/src/mediawiki.language", + "resources/src/mediawiki.messagePoster", "resources/src/mediawiki.page", "resources/src/mediawiki.special", "resources/src/mediawiki.toolbar", diff --git a/languages/i18n/en.json b/languages/i18n/en.json index bf65202..67629de 100644 --- a/languages/i18n/en.json +++ b/languages/i18n/en.json @@ -3538,6 +3538,7 @@ "feedback-error1": "Error: Unrecognized result from API", "feedback-error2": "Error: Edit failed", "feedback-error3": "Error: No response from API", + "feedback-error4": "Error: Unable to create MessagePoster for given feedback title", "feedback-message": "Message:", "feedback-subject": "Subject:", "feedback-submit": "Submit", diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json index f573d45..cc85d4f 100644 --- a/languages/i18n/qqq.json +++ b/languages/i18n/qqq.json @@ -3703,6 +3703,7 @@ "feedback-error1": "Error message, appears when an unknown error occurs submitting feedback", "feedback-error2": "Error message, appears when we could not add feedback", "feedback-error3": "Error message, appears when we lose our connection to the wiki", + "feedback-error4": "Error message, appears when mediawiki.feedback or one of its dependencies is misconfigured or there is a problem fetching one of the modules", "feedback-message": "Label for a textarea; signature refers to a Wikitext signature.\n{{Identical|Message}}", "feedback-subject": "Label for a text input\n{{Identical|Subject}}", "feedback-submit": "Button label\n{{Identical|Submit}}", diff --git a/maintenance/jsduck/categories.json b/maintenance/jsduck/categories.json index c0d0499..d15de64 100644 --- a/maintenance/jsduck/categories.json +++ b/maintenance/jsduck/categories.json @@ -22,6 +22,9 @@ "classes": [ "mw.Title", "mw.Uri", + "mw.messagePoster.factory", + "mw.messagePoster.MessagePoster", + "mw.messagePoster.WikitextMessagePoster", "mw.notification", "mw.Notification_", "mw.user", diff --git a/resources/Resources.php b/resources/Resources.php index 4464e4b..498b6df 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -875,7 +875,7 @@ 'scripts' => 'resources/src/mediawiki/mediawiki.feedback.js', 'styles' => 'resources/src/mediawiki/mediawiki.feedback.css', 'dependencies' => array( - 'mediawiki.api.edit', + 'mediawiki.messagePoster', 'mediawiki.Title', 'oojs-ui', ), @@ -894,6 +894,7 @@ 'feedback-error1', 'feedback-error2', 'feedback-error3', + 'feedback-error4', 'feedback-message', 'feedback-subject', 'feedback-submit', @@ -953,6 +954,26 @@ ), 'targets' => array( 'desktop', 'mobile' ), ), + 'mediawiki.messagePoster' => array( + 'scripts' => array( + 'resources/src/mediawiki.messagePoster/mediawiki.messagePoster.messagePosterFactory.js', + 'resources/src/mediawiki.messagePoster/mediawiki.messagePoster.MessagePoster.js', + ), + 'dependencies' => array( + 'oojs', + 'mediawiki.api', + ), + 'targets' => array( 'desktop', 'mobile' ), + ), + 'mediawiki.messagePoster.wikitext' => array( + 'scripts' => array( + 'resources/src/mediawiki.messagePoster/mediawiki.messagePoster.WikitextMessagePoster.js', + ), + 'dependencies' => array( + 'mediawiki.api.edit', + ), + 'targets' => array( 'desktop', 'mobile' ), + ), 'mediawiki.notification' => array( 'styles' => array( 'resources/src/mediawiki/mediawiki.notification.css', diff --git a/resources/src/mediawiki.messagePoster/mediawiki.messagePoster.MessagePoster.js b/resources/src/mediawiki.messagePoster/mediawiki.messagePoster.MessagePoster.js new file mode 100644 index 0000000..7489f1c --- /dev/null +++ b/resources/src/mediawiki.messagePoster/mediawiki.messagePoster.MessagePoster.js @@ -0,0 +1,35 @@ +( function ( mw ) { + /** + * This is the abstract base class for MessagePoster implementations. + * + * @abstract + * + * @param {mw.Title} title Title to post to + */ + mw.messagePoster.MessagePoster = function MwMessagePoster( title ) {}; + + OO.initClass( mw.messagePoster.MessagePoster ); + + /** + * Posts a message (with subject and body) to a talk page. + * + * @param {string} subject Subject/topic title; plaintext only (no wikitext or HTML) + * @param {string} body Body, as wikitext. Signature code will automatically be added + * by MessagePosters that require one, unless the message already contains the string + * ~~~. + * @return {jQuery.Promise} Promise completing when the post succeeds or fails. + * @return {Function} return.done + * @return {Function} return.fail + * @return {string} return.fail.primaryError Primary error code. For a mw.Api failure, + * this, should be 'api-fail'. + * @return {string} return.fail.secondaryError Secondary error code. For a mw.Api failure, + * this, should be mw.Api's code, e.g. 'http', 'ok-but-empty', or the error passed through + * from the server. + * @return {Mixed} return.fail.details Further details about the error + * + * @localdoc + * The base class currently does nothing, but could be used for shared analytics or + * something. + */ + mw.messagePoster.MessagePoster.prototype.post = function ( subject, body ) {}; +}( mediaWiki ) ); diff --git a/resources/src/mediawiki.messagePoster/mediawiki.messagePoster.WikitextMessagePoster.js b/resources/src/mediawiki.messagePoster/mediawiki.messagePoster.WikitextMessagePoster.js new file mode 100644 index 0000000..0bcb2dc --- /dev/null +++ b/resources/src/mediawiki.messagePoster/mediawiki.messagePoster.WikitextMessagePoster.js @@ -0,0 +1,56 @@ +( function ( mw, $ ) { + /** + * This is an implementation of MessagePoster for wikitext talk pages. + * + * @abstract + * @class + * + * @extends mw.messagePoster.MessagePoster + * + * @param {mw.Title} title Wikitext page in a talk namespace, to post to + */ + mw.messagePoster.WikitextMessagePoster = function MwWikitextMessagePoster( title ) { + this.api = new mw.Api(); + this.title = title; + }; + + OO.inheritClass( + mw.messagePoster.WikitextMessagePoster, + mw.messagePoster.MessagePoster + ); + + /** + * @inheritdoc + */ + mw.messagePoster.WikitextMessagePoster.prototype.post = function ( subject, body ) { + var dfd = $.Deferred(), promise = dfd.promise(); + + mw.messagePoster.WikitextMessagePoster.super.prototype.post.call( this, subject, body ); + + // Add signature if needed + if ( body.indexOf( '~~~' ) === -1 ) { + body += '\n\n~~~~'; + } + + this.api.newSection( + this.title, + subject, + body, + { redirect: true } + ).done( function ( resp, jqXHR ) { + if ( resp.edit.result === 'Success' ) { + dfd.resolve( resp, jqXHR ); + } else { + // mediawiki.api.js checks for resp.error. Are there actually cases where the + // request fails, but it's not caught there? + dfd.reject( 'api-unexpected' ); + } + } ).fail( function ( code, details ) { + dfd.reject( 'api-fail', code, details ); + } ); + + return promise; + }; + + mw.messagePoster.factory.register( 'wikitext', mw.messagePoster.WikitextMessagePoster ); +}( mediaWiki, jQuery ) ); diff --git a/resources/src/mediawiki.messagePoster/mediawiki.messagePoster.messagePosterFactory.js b/resources/src/mediawiki.messagePoster/mediawiki.messagePoster.messagePosterFactory.js new file mode 100644 index 0000000..a3b7ad4 --- /dev/null +++ b/resources/src/mediawiki.messagePoster/mediawiki.messagePoster.messagePosterFactory.js @@ -0,0 +1,116 @@ +( function ( mw, $ ) { + /** + * This is a factory for MessagePoster objects, which allows a pluggable to way to script leaving a + * talk page message. + * + * @class mw.messagePoster.factory + * @singleton + */ + function MwMessagePosterFactory() { + this.api = new mw.Api(); + this.contentModelToClass = {}; + } + + OO.initClass( MwMessagePosterFactory ); + + // Note: This registration scheme is currently not compatible with LQT, since that doesn't + // have its own content model, just islqttalkpage. LQT pages will be passed to the wikitext + // MessagePoster. + /** + * Registers a MessagePoster subclass for a given content model. + * + * @param {string} contentModel Content model of pages this MessagePoster can post to + * @param {Function} messagePosterConstructor Constructor for MessagePoster + */ + MwMessagePosterFactory.prototype.register = function ( contentModel, messagePosterConstructor ) { + if ( this.contentModelToClass[contentModel] !== undefined ) { + throw new Error( 'The content model \'' + contentModel + '\' is already registered.' ); + } + + this.contentModelToClass[contentModel] = messagePosterConstructor; + }; + + /** + * Unregisters a given content model + * This is exposed for testing and should not normally be needed. + * + * @param {string} contentModel Content model to unregister + */ + MwMessagePosterFactory.prototype.unregister = function ( contentModel ) { + delete this.contentModelToClass[contentModel]; + }; + + /** + * Creates a MessagePoster, given a title. A promise for this is returned. + * This works by determining the content model, then loading the corresponding + * module (which will register the MessagePoster class), and finally constructing it. + * + * This does not require the message and should be called as soon as possible, so it does the + * API and ResourceLoader requests in the background. + * + * @param {mw.Title} title Title that will be posted to + * @return {jQuery.Promise} Promise for the MessagePoster + * @return {Function} return.done Called if MessagePoster is retrieved + * @return {mw.messagePoster.MessagePoster} return.done.poster MessagePoster + * @return {Function} return.fail Called if MessagePoster could not be constructed + * @return {string} return.fail.errorCode String error code + */ + MwMessagePosterFactory.prototype.create = function ( title ) { + var pageId, page, contentModel, moduleName, dfd = $.Deferred(), + promise = dfd.promise(), factory = this; + + this.api.get( { + action: 'query', + prop: 'info', + indexpageids: 1, + titles: title.getPrefixedDb() + } ).done( function ( result ) { + var messagePoster; + + if ( result && + result.query && + result.query.pageids && + result.query.pageids.length > 0 ) { + + pageId = result.query.pageids[0]; + page = result.query.pages[pageId]; + + contentModel = page.contentmodel; + moduleName = 'mediawiki.messagePoster.' + contentModel; + mw.loader.using( moduleName ).done( function () { + messagePoster = factory.createForContentModel( + contentModel, + title + ); + dfd.resolve( messagePoster ); + } ).fail( function () { + dfd.reject( 'failed-to-load-module', 'Failed to load the \'' + moduleName + '\' module' ); + } ); + } else { + dfd.reject( 'unexpected-response', 'Unexpected API response' ); + } + } ).fail( function ( errorCode, details ) { + dfd.reject( 'mw.Api content model query: ' + errorCode, details ); + } ); + + return promise; + }; + + /** + * Creates a MessagePoster instance, given a title and content model + * + * @param {string} contentModel Content model of title + * @param {mw.Title} title Title being posted to + * + * @return + * + * @private + */ + MwMessagePosterFactory.prototype.createForContentModel = function ( contentModel, title ) { + return new this.contentModelToClass[contentModel]( title ); + }; + + mw.messagePoster = { + factory: new MwMessagePosterFactory() + }; +}( mediaWiki, jQuery ) ); diff --git a/resources/src/mediawiki/mediawiki.feedback.js b/resources/src/mediawiki/mediawiki.feedback.js index 9a671c0..c090a45 100644 --- a/resources/src/mediawiki/mediawiki.feedback.js +++ b/resources/src/mediawiki/mediawiki.feedback.js @@ -36,7 +36,6 @@ * @class * @constructor * @param {Object} [config] Configuration object - * @cfg {mw.Api} [api] if omitted, will just create a standard API * @cfg {mw.Title} [title="Feedback"] The title of the page where you collect * feedback. * @cfg {string} [dialogTitleMessageKey="feedback-dialog-title"] Message key for the @@ -53,11 +52,12 @@ mw.Feedback = function MwFeedback( config ) { config = config || {}; - this.api = config.api || new mw.Api(); this.dialogTitleMessageKey = config.dialogTitleMessageKey || 'feedback-dialog-title'; // Feedback page title this.feedbackPageTitle = config.title || new mw.Title( 'Feedback' ); + + this.messagePosterPromise = mw.messagePoster.factory.create( this.feedbackPageTitle ); // Links this.bugsTaskSubmissionLink = config.bugsLink || '//phabricator.wikimedia.org/maniphest/task/create/'; @@ -120,6 +120,7 @@ case 'error1': case 'error2': case 'error3': + case 'error4': dialogConfig = { title: mw.msg( 'feedback-error-title' ), message: mw.msg( 'feedback-' + status ), @@ -147,8 +148,8 @@ * Modify the display form, and then open it, focusing interface on the subject. * * @param {Object} [contents] Prefilled contents for the feedback form. - * @param {string} [contents.subject] The subject of the feedback - * @param {string} [contents.message] The content of the feedback + * @param {string} [contents.subject] The subject of the feedback, as plaintext + * @param {string} [contents.message] The content of the feedback, as wikitext */ mw.Feedback.prototype.launch = function ( contents ) { // Dialog @@ -171,7 +172,7 @@ { title: mw.msg( this.dialogTitleMessageKey ), settings: { - api: this.api, + messagePosterPromise: this.messagePosterPromise, title: this.feedbackPageTitle, dialogTitleMessageKey: this.dialogTitleMessageKey, bugsTaskSubmissionLink: this.bugsTaskSubmissionLink, @@ -339,7 +340,7 @@ this.feedbackMessageInput.setValue( data.contents.message ); this.status = ''; - this.api = settings.api; + this.messagePosterPromise = settings.messagePosterPromise; this.setBugReportLink( settings.bugsTaskSubmissionLink ); this.feedbackPageTitle = settings.title; this.feedbackPageName = settings.title.getNameText(); @@ -418,37 +419,13 @@ message = userAgentMessage + message; } - // Add signature if needed - if ( message.indexOf( '~~~' ) === -1 ) { - message += '\n\n~~~~'; - } - - // Post the message, resolving redirects + // Post the message this.pushPending(); - this.api.newSection( - this.feedbackPageTitle, - subject, - message, - { redirect: true } - ) - .done( function ( result ) { - if ( result.edit.result === 'Success' ) { - fb.status = 'submitted'; - } else { - fb.status = 'error1'; - } - fb.popPending(); - fb.close(); - } ) - .fail( function ( code, result ) { - if ( code === 'http' ) { - fb.status = 'error3'; - // ajax request failed - mw.log.warn( 'Feedback report failed with HTTP error: ' + result.textStatus ); - } else { - fb.status = 'error2'; - mw.log.warn( 'Feedback report failed with API error: ' + code ); - } + this.messagePosterPromise.done( function ( poster ) { + fb.postMessage( poster, subject, message ); + } ).fail( function () { + fb.status = 'error4'; + mw.log.warn( 'Feedback report failed because MessagePoster could not be fetched' ); fb.popPending(); fb.close(); } ); @@ -459,6 +436,42 @@ }; /** + * Posts the message, then pops the pending state + * + * @private + * + * @param {mw.messagePoster.MessagePoster} poster Poster implementation used to leave feedback + * @param {string} subject Subject of message + * @param {string} message Body of message + */ + mw.Feedback.Dialog.prototype.postMessage = function ( poster, subject, message ) { + var fb = this; + + poster.post( + subject, + message + ).done( function () { + fb.status = 'submitted'; + } ).fail( function ( mainCode, secondaryCode, details ) { + if ( mainCode === 'api-fail' ) { + if ( secondaryCode === 'http' ) { + fb.status = 'error3'; + // ajax request failed + mw.log.warn( 'Feedback report failed with HTTP error: ' + details.textStatus ); + } else { + fb.status = 'error2'; + mw.log.warn( 'Feedback report failed with API error: ' + secondaryCode ); + } + } else { + fb.status = 'error1'; + } + } ).always( function () { + fb.popPending(); + fb.close(); + } ); + }; + + /** * @inheritdoc */ mw.Feedback.Dialog.prototype.getTeardownProcess = function ( data ) { diff --git a/tests/qunit/QUnitTestResources.php b/tests/qunit/QUnitTestResources.php index 8430413..61fb970 100644 --- a/tests/qunit/QUnitTestResources.php +++ b/tests/qunit/QUnitTestResources.php @@ -64,6 +64,7 @@ 'tests/qunit/data/mediawiki.jqueryMsg.data.js', 'tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js', 'tests/qunit/suites/resources/mediawiki/mediawiki.jscompat.test.js', + 'tests/qunit/suites/resources/mediawiki/mediawiki.messagePosterFactory.test.js', 'tests/qunit/suites/resources/mediawiki/mediawiki.test.js', 'tests/qunit/suites/resources/mediawiki/mediawiki.Title.test.js', 'tests/qunit/suites/resources/mediawiki/mediawiki.template.test.js', diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.messagePosterFactory.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.messagePosterFactory.test.js new file mode 100644 index 0000000..4ed087a --- /dev/null +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.messagePosterFactory.test.js @@ -0,0 +1,28 @@ +( function ( mw ) { + var TEST_MODEL = 'test-content-model'; + + QUnit.module( 'mediawiki', QUnit.newMwEnvironment( { + teardown: function () { + mw.messagePosterFactory.unregister( TEST_MODEL ); + } + } ) ); + + QUnit.test( 'register', 2, function ( assert ) { + var testMessagePosterConstructor = function () {}; + + mw.messagePosterFactory.register( TEST_MODEL, testMessagePosterConstructor ); + assert.strictEqual( + mw.messagePosterFactory.contentModelToClass[TEST_MODEL], + testMessagePosterConstructor, + 'Constructor is registered' + ); + + assert.throws( + function () { + mw.messagePosterFactory.register( TEST_MODEL, testMessagePosterConstructor ); + }, + new RegExp( 'The content model \'' + TEST_MODEL + '\' is already registered.' ), + 'Throws exception is same model is registered a second time' + ); + } ); +}( mediaWiki ) ); -- To view, visit https://gerrit.wikimedia.org/r/200801 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: Ic69acafb24aa737536fe3a074e1958690732f0a7 Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/core Gerrit-Branch: master Gerrit-Owner: Mattflaschen <mflasc...@wikimedia.org> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits