User:Mr. Stradivarius/gadgets/Draftify.js

Source: Wikipedia, the free encyclopedia.
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
// <nowiki>
/*
 * Draftify
 *
 * This gadget allows you to move a user page to a different location (usually
 * the Draft namespace), notify the user, and optionally soft-block them.
 *
 * To install the script, add the following to your personal .js page:
 
importScript( 'User:Mr. Stradivarius/gadgets/Draftify.js' ); // Linkback: [[User:Mr. Stradivarius/gadgets/Draftify.js]]
 
 * Author: Mr. Stradivarius
 * Licence: MIT
 *
 * The MIT License (MIT)
 *
 * Copyright (c) 2015 Mr. Stradivarius
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

mw.loader.using( [
	'mediawiki.api',
	'mediawiki.jqueryMsg',
	'mediawiki.Title',
	'mediawiki.util',
	'oojs-ui'
], function () {
	"use strict";

	var config, ApiManager, Dialog;

	/**************************************************************************
	 *                     MediaWiki config and exit check
	 **************************************************************************/

	// A global object that stores all the page config, defaults, and user
	// preferences.
	config = {};

	config.mw = mw.config.get( [
		'wgTitle',
		'wgNamespaceNumber',
		'wgArticleId',
		'wgFormattedNamespaces',
		'wgPageContentModel',
		'wgPageName',
		'wgUserName',
		'wgUserGroups',
		'wgRelevantUserName',
	] );

	// If we're not going to work on the page, exit as soon as possible.
	if (
			config.mw.wgNamespaceNumber !== 2 && config.mw.wgNamespaceNumber !== 3 ||
			config.mw.wgArticleId === 0 || // Page doesn't exist
			!config.mw.wgRelevantUserName || // Userspace of non-existent user
			config.mw.wgUserName === config.mw.wgRelevantUserName || // User's own userspace
			config.mw.wgPageContentModel !== 'wikitext' || // Exclude user JS/CSS
			mw.util.getParamValue( 'redirect', window.location.href ) === 'no' // Current page is a redirect
	) {
		return;
	}

	/**************************************************************************
	 *                              Messages
	 *
	 * This is the part you need to edit to localise the gadget.
	 **************************************************************************/

	config.defaultMessages = {
		// The label on the portlet link
		'dfy-portlet-label': 'Draftify',

		// The portlet link tooltip text
		'dfy-portlet-tooltip': 'Move this draft and notify the user',

		// The prefix that target draft pages must start with.
		'dfy-draft-prefix': 'Draft:',

		// The edit summary to use when moving the page.
		'dfy-move-summary': 'the [[WP:DRAFTS|Draft namespace]] is the preferred location for [[WP:AFC|Articles for Creation]] submissions',

		// The template invocation to use when tagging drafts.
		// $1 - the username of the user whose userspace the draft was in
		'dfy-tag-template': '{{subst:AFC draft|1=$1}}',

		// The edit summary to use when tagging drafts with dfy-tag-template.
		// $1 - the username of the user whose userspace the draft was in
		'dfy-tag-summary': 'Tag page as draft belonging to [[:User:$1]]',

		// The template invocation to use when soft-blocking users.
		'dfy-softblock-template': '{{subst:uw-softerblock|sig=yes}}',
		
		// The edit summary to use when soft-blocking users.
		'dfy-softblock-summary': '{{uw-softerblock}} <!-- Promotional username, soft block -->',

		// The template invocation to use when notifying users that their
		// draft has been moved.
		// $1 - The new page name after the move
		// $2 - The current page name
		'dfy-notify-template': '{{subst:uw-draftmoved|1=$1|from=$2}} ~~~~',

		// The template invocation to use when suggesting the user should
		// change their username.
		'dfy-suggestrename-template': '{{subst:uw-username|1=it gives the impression that your account represents a group, organization or website}} ~~~~',

		// Edit summary to leave when notifying the user that their draft has
		// been moved.
		'dfy-notify-summary-moveonly': 'Your draft page has been moved',

		// Edit summary to leave when notifying the user that their draft has
		// been moved and suggesting that they rename their account.
		'dfy-notify-summary-suggestrename': 'Your draft page has been moved, and you may need to change your [[WP:U|username]]',

		// Edit summary to leave when notifying the user that their draft has
		// been moved and that they have been soft-blocked.
		'dfy-notify-summary-softblock': 'Your draft page has been moved, and you have been indefinitely blocked from editing because your [[WP:U|username]] gives the impression that the account represents a group, organization or website',

		// Boilerplate text to add at the end of the edit summary.
		'dfy-summary-suffix': '([[WP:DFY|DFY]])',

		// The talk heading to be used for the talkpage notification.
		// $1 - The current month name, as defined in config.months.
		// $2 - The current year
		'dfy-talk-heading': '$1 $2',

		// Label for the text input where the user specifies the new draft name.
		'dfy-title-input-label': 'New draft name',

		// Label for the redirect checkbox
		'dfy-redirect-checkbox-label': 'Leave a redirect behind',

		// Label for the notification checkbox
		// $1 - The user who the draft belongs to
		'dfy-notify-checkbox-label': 'Notify the user of the page move',

		// Label for the checkbox for adding advice about renames
		// $1 - The user who the draft belongs to
		'dfy-suggestrename-checkbox-label': 'Suggest that the user change their username',

		// Label for the redirect checkbox
		'dfy-softblock-checkbox-label': 'Soft-block the user and leave a block notice',

		// Label for the watch user talk checkbox
		'dfy-watch-user-talk-checkbox-label': 'Watch user talk page',

		// Label for the watch draft checkbox
		'dfy-watch-draft-checkbox-label': 'Watch source page and target page',

		// Label for the move progress indicator
		'dfy-move-progress-label': 'Moving draft...',

		// Label for the tag progress indicator
		'dfy-tag-progress-label': 'Tagging draft...',

		// Label for the softblock progress indicator
		// $1 - the user being blocked
		'dfy-softblock-progress-label': 'Soft-blocking $1...',

		// Label for the notify progress indicator
		// $1 - the user being notified
		'dfy-notify-progress-label': 'Notifying $1...',

		// Label for the progress indicator for opening the talk page
		// $1 - the talk page being opened
		'dfy-open-talk-progress-label': 'Opening user talk...',

		// Value for completed progress indicators.
		'dfy-progress-success': 'Done.',

		// Value for failed progress indicators.
		// $1 - the error message
		'dfy-progress-failed': 'Error: $1',

		// Value for failed progress indicators with unknown errors.
		'dfy-progress-unknown-error': 'An unknown error occurred.',

		// Error message for non-existent pages
		// $1 - the page name
		'dfy-nonexistent-page-error': 'The page "$1" does not exist.',
	};

	config.months = [
		'January',
		'February',
		'March',
		'April',
		'May',
		'June',
		'July',
		'August',
		'September',
		'October',
		'November',
		'December'
	];

	/**************************************************************************
	 *                              Config
	 *
	 * Get user preferences, set messages, and deal with other per-user config.
	 **************************************************************************/

	// Get the raw user preferences.
	config.rawPrefs = typeof window.Draftify === 'object' ? window.Draftify : {};

	// These messages are configurable by the user.
	config.configurableMessages = {
		movesummary: 'dfy-move-summary',
		tagtemplate: 'dfy-tag-template',
		tagsummary: 'dfy-tag-summary',
		notifytemplate: 'dfy-notify-template',
		notifysummarymoveonly: 'dfy-notify-summary-moveonly',
		notifysummarysuggestrename: 'dfy-notify-summary-suggestrename',
		notifysummarysoftblock: 'dfy-notify-summary-softblock',
		softblocktemplate: 'dfy-softblock-template',
		softblocksummary: 'dfy-softblock-summary',
		suggestrenametemplate: 'dfy-suggestrename-template'
	};

	// Get the messages to use from the defaults and the user preferences.
	config.messages = {};
	$.each( config.defaultMessages, function ( key, value ) {
		config.messages[ key ] = value;
	} );
	$.each( config.configurableMessages, function ( prefKey, messageKey ) {
		if ( typeof config.rawPrefs[ prefKey ] === 'string' ) {
			config.messages[ messageKey ] = config.rawPrefs[ prefKey ];
		}
	} );

	// Set the messages for use by the MediaWiki message library.
	mw.messages.set( config.messages );

	// Define the default values for non-messages that can be set as
	// preferences.
	config.defaults = {
		redirect: false,
		notify: true,
		softblock: false,
		suggestrename: false,
		watchusertalk: true,
		watchdraft: true,
		menulocation: 'p-cactions',
		menuposition: null
	};

	// Define aliases for config keys.
	config.aliases = {
		watchdraft: [ "watch" ] // For backwards compatibility with old options
	}

	// Find the preferences to use from the defaults, aliases, and user preferences.
	config.prefs = {};
	$.each( config.defaults, function ( key, value ) {
		if ( config.rawPrefs[ key ] !== undefined ) {
			config.prefs[ key ] = config.rawPrefs[ key ];
		} else if ( config.aliases[ key ] !== undefined ) {
			$.each( config.aliases[ key ], function ( index, alias ) {
				if ( config.rawPrefs[ alias ] !== undefined ) {
					config.prefs[ key ] = config.rawPrefs[ alias ];
					return false; // Break the loop if we find an alias
				}
			} );
		}
		if ( config.prefs[ key ] === undefined ) {
			config.prefs[ key ] = value;
		}
	} );

	/**************************************************************************
	 *                           ApiManager class
	 *
	 * This class is the interface to the MediaWiki API and the config. Other
	 * classes should go through the ApiManager rather than access the config
	 * directly.
	 **************************************************************************/

	ApiManager = function () {
		var currentUserIsAdmin;
		this.api = new mw.Api();
		this.currentSubpage = config.mw.wgTitle.replace( /^.*\//, '' );
		this.currentDraftTitle = new mw.Title(
			config.mw.wgTitle,
			config.mw.wgNamespaceNumber
		);
		this.targetDraftTitle = null;
		this.userTalkTitle = new mw.Title( this.getTargetUser(), 3 );
	};

	OO.initClass( ApiManager );

	// Used to validate the title field
	ApiManager.static.titleValidationRegex = /^[^|<>{}\[\]#]+$/;

	ApiManager.prototype.getPreference = function ( key ) {
		return config.prefs[ key ];
	};

	// Fetches the current user's rights from the API and stores the relevant
	// ones in the ApiManager object. We don't check for things
	// like page protection status, so having the right to do something doesn't
	// mean that doing it will be successful.
	// Returns a jQuery.promise object.
	ApiManager.prototype.setRights = function () {
		var apiManager = this;
		return mw.user.getRights().then(
			// Done filter
			function ( rights ) {
				apiManager.rights = {
					suppressredirect: $.inArray( 'suppressredirect', rights ) !== -1,
					softblock: $.inArray( 'block', rights ) !== -1
				};
			},
			// Fail filter
			function () {
				apiManager.rights = {};
			}
		);
	};

	ApiManager.prototype.currentUserCan = function ( action ) {
		var right = this.rights[ action ];
		if ( right === undefined ) {
			return true;
		} else {
			return right;
		}
	};

	ApiManager.prototype.getCurrentSubpage = function () {
		return this.currentSubpage;
	};

	ApiManager.prototype.getTargetUser = function () {
		return config.mw.wgRelevantUserName;
	};

	ApiManager.prototype.getTitleValidationRegex = function () {
		return this.constructor.static.titleValidationRegex;
	};

	ApiManager.prototype.getCurrentDraftTitle = function () {
		return this.currentDraftTitle;
	};

	ApiManager.prototype.getTargetDraftTitle = function () {
		return this.targetDraftTitle;
	};

	ApiManager.prototype.setTargetDraftTitle = function ( titleObj ) {
		this.targetDraftTitle = titleObj;
	};

	ApiManager.prototype.addPortletLink = function () {
		return mw.util.addPortletLink(
			this.getPreference( 'menulocation' ),
			'#',
			mw.message( 'dfy-portlet-label' ).plain(),
			'ca-draftify',
			mw.message( 'dfy-portlet-tooltip' ).plain(),
			null,
			this.getPreference( 'menuposition' )
		);
	};

	ApiManager.prototype.getNotificationDate = function () {
		// We cache the result of this method so that we will always get the
		// same date string as we used for the talk notification, even if a
		// user happened to have the page open over a month boundary.
		var date, month;
		if ( !this.notificationDate ) {
			date = new Date();
			month = config.months[ date.getMonth() ];
			this.notificationDate = mw.message(
				'dfy-talk-heading',
				month,
				date.getFullYear()
			).text();
		}
		return this.notificationDate;
	};

	ApiManager.prototype.getUserTalkTitle = function () {
		return this.userTalkTitle;
	};

	ApiManager.prototype.openTalkPage = function () {
		window.open(
			this.getUserTalkTitle().getUrl() +
				'#' +
				mw.util.wikiUrlencode( this.getNotificationDate() ),
			'_top'
		);
	};

	ApiManager.prototype.getPageContent = function ( options ) {
		options = options || {};
		var self = this;
		return $.Deferred( function ( deferred ) {
			self.api.get( {
				format: 'json',
				action: 'query',
				prop: 'revisions',
				rvprop: 'content',
				indexpageids: '',
				titles: options.title,
				redirects: ''
			} ).then( function ( obj ) {
				var newTitle, content,
					pageId = obj.query.pageids[ 0 ];

				// If we got a redirect from the GET request, follow it.
				if ( obj.query.redirects ) {
					newTitle = obj.query.redirects[ 0 ].to;
				} else {
					newTitle = options.title;
				}

				// Get the page content.
				if ( pageId === '-1' ) {
					if ( options.allowNonexistent ) {
						content = '';
					} else {
						// We got a non-existent page where we weren't
						// expecting one, so bail.
						return deferred.reject( 'dfy-nonexistent-page-error', { 'error': {
							'id': 'dfy-nonexistent-page-error',
							'info': mw.message(
								'dfy-nonexistent-page-error',
								newTitle
							).text()
						} } );
					}
				} else {
					content = obj.query.pages[ pageId ].revisions[ 0 ][ '*' ];
				}

				return deferred.resolve( {
					title: newTitle,
					content: content
				} );
			} );
		} ).promise();
	};

	ApiManager.prototype.editPage = function ( options ) {
		options = options || {};
		return this.api.postWithEditToken( {
			format: 'json',
			action: 'edit',
			title: options.title,
			summary: options.editSummary + ' ' + mw.message( 'dfy-summary-suffix' ).plain(),
			notminor: '',
			text: options.content,
			watchlist: options.watch ? 'watch' : 'unwatch'
		} );
	};

	ApiManager.prototype.moveDraft = function ( options ) {
		options = options || {};
		var self = this,
			targetTitle = this.getTargetDraftTitle();
		if ( !targetTitle ) {
			throw new Error( 'moveDraft was called but no target title object was available' );
		}
		// Get the page content for tagging before we move the page, otherwise
		// we risk trying to get the content from the draft page before it is
		// moved, resulting in page blanking.
		return self.getPageContent( {
			title: self.currentDraftTitle.getPrefixedText(),
			allowNonexistent: false
		} ).then( function ( contentObj ) {
			var apiArgs = {
				action: 'move',
				format: 'json',
				from: self.getCurrentDraftTitle().getPrefixedText(),
				to: targetTitle.getPrefixedText(),
				reason: mw.message( 'dfy-move-summary' ).plain() +
					' ' +
					mw.message( 'dfy-summary-suffix' ).plain(),
				watchlist: options.watchdraft ? 'watch' : 'unwatch'
			};
			if ( !options.redirect ) {
				apiArgs.noredirect = 1;
			}
			return self.api.postWithEditToken( apiArgs ).then( function () {
				return contentObj;
			} );
		} );
	};

	ApiManager.prototype.tagDraft = function ( options, contentObj ) {
		if ( !contentObj ) {
			throw new Error( 'tagDraft was called without contentObj' );
		}
		var content = contentObj.content,
			tag =  mw.message( 'dfy-tag-template', this.getTargetUser() ).plain();

		// Transform the content
		if ( /^\s*{{/.test( content ) ) {
			// The page starts with a template.
			content = tag + '\n' + content;
		} else {
			content = tag + '\n\n' + content;
		}

		return this.editPage( {
			title: this.getTargetDraftTitle().getPrefixedText(),
			editSummary: mw.message( 'dfy-tag-summary', this.getTargetUser() ).plain(),
			content: content,
			watch: options.watchdraft
		} );
	};

	ApiManager.prototype.softBlockUser = function ( options ) {
		return this.api.postWithEditToken( {
			format: 'json',
			action: 'block',
			user: this.apiManager.getTargetUser(),
			expiry: 'infinite',
			reason: mw.message( 'dfy-softblock-summary' ).plain(),
			allowusertalk: 1
		} );
	};

	ApiManager.prototype.notifyUser = function ( options ) {
		options = options || {};
		var summary,
			self = this;

		if ( options.softblock ) {
			summary = mw.message( 'dfy-notify-summary-softblock' ).plain();
		} else if ( options.suggestrename ) {
			summary = mw.message( 'dfy-notify-summary-suggestrename' ).plain();
		} else {
			summary = mw.message( 'dfy-notify-summary-moveonly' ).plain();
		}

		return self.getPageContent( {
			title: self.getUserTalkTitle().getPrefixedText(),
			allowNonexistent: true
		} ).then( function ( contentObj ) {
			// Generate the new content.
			// First, add a heading for the current month if it is not already
			// the last heading on the page.
			var lastHeading, headings,
				content = contentObj.content,
				notificationDate = self.getNotificationDate();

			if ( /\S/.test( content ) ) {
				// Separate the notice from whatever came before.
				content += '\n\n';
			}

			// Add a heading for the current month if there isn't one already.
			headings = content.match( /^==[^=].*==[ \t]*$/gm );
			if ( headings ) {
				lastHeading = headings[ headings.length - 1 ].slice( 2, -2 ).trim();
			}
			if ( !lastHeading || lastHeading !== notificationDate ) {
				content += '== ' + notificationDate + ' ==\n\n';
			}

			// Add the notification templates.
			content += mw.message(
				'dfy-notify-template',
				self.getTargetDraftTitle() ? self.getTargetDraftTitle().getPrefixedText() : '',
				self.getCurrentDraftTitle().getPrefixedText()
			).plain();
			if ( options.softblock ) {
				content += '\n\n' + mw.message( 'dfy-softblock-template' ).plain();
			} else if ( options.suggestrename ) {
				content += '\n\n' + mw.message( 'dfy-suggestrename-template' ).plain();
			}
			
			return self.editPage( {
				title: contentObj.title,
				editSummary: summary,
				content: content,
				watch: options.watchusertalk
			} );
		} );

	};

	ApiManager.prototype.submit = function ( options ) {
		var i, len,
			self = this,
			actions = [],
			promises = {},
			whenArgs = [];

		// Make handler function for each promise that resolves successfully.
		function makeDoneHandler( i ) {
			return function ( obj ) {
				return self[ actions[ i ].method ]( options, obj );
			};
		}

		try {
			this.setTargetDraftTitle( new mw.Title(
				mw.message( 'dfy-draft-prefix' ).text() + options.title
			) );

			// Specify the order in which the actions should be performed.
			actions.push( { action: 'move', method: 'moveDraft' } );
			actions.push( { action: 'tag', method: 'tagDraft' } );
			if ( options.softblock ) {
				actions.push( { action: 'softblock', method: 'softBlockUser' } );
			}
			if ( options.notify ) {
				actions.push( { action: 'notify', method: 'notifyUser' } );
			}

			// Make a chain of promises to perform the actions, and reference the
			// promises in the promises object.
			promises[ actions[ 0 ].action ] = self[ actions[ 0 ].method ]( options );
			for ( i = 1, len = actions.length; i < len; i++ ) {
				promises[ actions[ i ].action ] = promises[ actions[ i - 1 ].action ].then(
					makeDoneHandler( i )
				);
			}
		} catch ( e ) {
			// Create a failed promise with the error info so that users will
			// see the error message when they click submit.
			promises.move = $.Deferred().reject( 'dfy-submit-error', { 'error': {
				'id': 'dfy-submit-error',
				'info': e.message || mw.message( 'dfy-progress-unknown-error' ).text()
			} } ).promise();
		}

		// Create an "all" promise that tracks the overall status of the
		// submission process.
		$.each( promises, function ( key, promise ) {
			whenArgs.push( promise );
		} );
		promises.all = $.when.apply( this, whenArgs );

		return promises;
	};

	/**************************************************************************
	 *                           Dialog class
	 **************************************************************************/

	Dialog = function ( options ) {
		options = options || {};
		this.apiManager = new ApiManager();
		this.isLoaded = false;
		this.hasBeenSubmitted = false;
		Dialog.super.call( this, options );
	};

	OO.inheritClass( Dialog, OO.ui.ProcessDialog );

	Dialog.static.name = 'DraftifyDialog';
	Dialog.static.title = 'Draftify';

	Dialog.static.actions = [
		{ action: 'submit', label: 'Submit', flags: [ 'primary', 'constructive' ] },
		{ label: 'Cancel', flags: 'safe' }
	];

	Dialog.prototype.getApiManager = function () {
		return this.apiManager;
	};

	Dialog.prototype.getBodyHeight = function () {
		return 300;
	};

	Dialog.prototype.initialize = function () {
		var apiManager = this.getApiManager();
		Dialog.super.prototype.initialize.apply( this, arguments );

		// Initialize edit panel
		this.editPanel = new OO.ui.PanelLayout( {
			expanded: false
		} );
		this.editFieldset = new OO.ui.FieldsetLayout( {
			classes: [ 'container' ]
		} );
		this.editPanel.$element.append( this.editFieldset.$element );

		// Initialize title text input widget
		this.titleInput = new OO.ui.TextInputWidget( {
			// TODO: Fix the label mess in OOjs-ui so that this can be
			// uncommented.
			// label: mw.message( 'dfy-draft-prefix' ).plain(),
			// labelPosition: 'before',
			validate: apiManager.getTitleValidationRegex(),
			value: apiManager.getCurrentSubpage()
		} );

		// Initialize checkbox widgets
		this.redirectCheckbox = new OO.ui.CheckboxInputWidget();
		this.redirectCheckbox.setSelected( true );
		this.redirectCheckbox.setDisabled( true );
		this.notifyCheckbox = new OO.ui.CheckboxInputWidget( {
			selected: apiManager.getPreference( 'notify' )
		} );
		this.suggestrenameCheckbox = new OO.ui.CheckboxInputWidget( {
			selected: apiManager.getPreference( 'suggestrename' )
		} );
		this.softblockCheckbox = new OO.ui.CheckboxInputWidget();
		this.softblockCheckbox.setDisabled( true );
		this.watchUserTalkCheckbox = new OO.ui.CheckboxInputWidget( {
			selected: apiManager.getPreference( 'watchusertalk' )
		} );
		this.watchDraftCheckbox = new OO.ui.CheckboxInputWidget( {
			selected: apiManager.getPreference( 'watchdraft' )
		} );

		// Add widgets to the edit fieldset
		this.editFieldset.addItems( [
			new OO.ui.FieldLayout( this.titleInput, {
				label: mw.message( 'dfy-title-input-label' ).plain()
			} ),
			new OO.ui.FieldLayout( this.redirectCheckbox, {
				label: mw.message( 'dfy-redirect-checkbox-label' ).plain(),
				align: 'inline'
			} ),
			new OO.ui.FieldLayout( this.notifyCheckbox, {
				// TODO: Use {{GENDER}} with the relevant username. I had a go
				// at this, but apparently jqueryMsg doesn't like me.
				label: mw.message( 'dfy-notify-checkbox-label' ).plain(),
				align: 'inline'
			} ),
			new OO.ui.FieldLayout( this.suggestrenameCheckbox, {
				// TODO: Use {{GENDER}}
				label: mw.message( 'dfy-suggestrename-checkbox-label' ).plain(),
				align: 'inline'
			} ),
			new OO.ui.FieldLayout( this.softblockCheckbox, {
				// TODO: Use {{GENDER}}
				label: mw.message( 'dfy-softblock-checkbox-label' ).plain(),
				align: 'inline'
			} ),
			new OO.ui.FieldLayout( this.watchUserTalkCheckbox, {
				label: mw.message( 'dfy-watch-user-talk-checkbox-label' ).plain(),
				align: 'inline'
			} ),
			new OO.ui.FieldLayout( this.watchDraftCheckbox, {
				label: mw.message( 'dfy-watch-draft-checkbox-label' ).plain(),
				align: 'inline'
			} )
		] );

		// Initialize submit panel. The progress fields aren't added to the
		// fieldset yet - they will be added dynamically when the user submits
		// the form.
		this.submitPanel = new OO.ui.PanelLayout( {
			$: this.$,
			expanded: false
		} );
		this.submitFieldset = new OO.ui.FieldsetLayout( {
			classes: [ 'container' ]
		} );
		this.submitPanel.$element.append( this.submitFieldset.$element );
		this.moveProgressLabel = new OO.ui.LabelWidget();
		this.moveProgressField = new OO.ui.FieldLayout( this.moveProgressLabel );
		this.tagProgressLabel = new OO.ui.LabelWidget();
		this.tagProgressField = new OO.ui.FieldLayout( this.tagProgressLabel );
		this.softblockProgressLabel = new OO.ui.LabelWidget();
		this.softblockProgressField = new OO.ui.FieldLayout( this.softblockProgressLabel );
		this.notifyProgressLabel = new OO.ui.LabelWidget();
		this.notifyProgressField = new OO.ui.FieldLayout( this.notifyProgressLabel );
		this.openTalkProgressField = new OO.ui.FieldLayout( new OO.ui.LabelWidget() );

		// Initialize stack widget
		this.stackLayout= new OO.ui.StackLayout( {
			items: [ this.editPanel, this.submitPanel ],
			padded: true
		} );

		// Add widgets to the DOM
		this.$body.append( this.stackLayout.$element );
	};

	Dialog.prototype.getReadyProcess = function ( data ) {
		data = data || {};
		var dialog = this;
		return Dialog.super.prototype.getReadyProcess.call( this, data )
		.next( function () {
			if ( !dialog.isLoaded ) {
				dialog.pushPending();
				dialog.actions.setAbilities( { submit: false } );
				dialog.getApiManager().setRights()
				.done( function () {
					dialog.onLoad();
					dialog.actions.setAbilities( { submit: true } );
					dialog.popPending();
				} )
				.fail( function () {
					dialog.popPending();
					// TODO: use the pretty OOjs-UI error formatting.
					alert( 'There was a problem fetching your user rights from the API.' );
					/*
					return [ new OO.ui.Error(
						'There was a problem fetching your user rights from the API',
						{ recoverable: false }
					) ];
					*/
				} );
			}
			if ( dialog.hasBeenSubmitted ) {
				// The dialog has already been submitted when it was previously
				// opened, so disable the Submit button.
				dialog.actions.setAbilities( { submit: false } );
			} else {
				// XXX: Workaround for text bunching issue in the title input.
				// Remove once this is fixed in OOjs-UI.
				this.titleInput.focus();
				// Make the cursor appear after the value instead of before.
				var val = this.titleInput.getValue();
				this.titleInput.setValue( '' );
				this.titleInput.setValue( val );
			}
		}, this );
	};

	Dialog.prototype.onLoad = function () {
		var apiManager = this.getApiManager();

		// Set checkbox statuses
		if ( apiManager.currentUserCan( 'suppressredirect' ) ) {
			this.redirectCheckbox.setSelected(
				apiManager.getPreference( 'redirect' )
			);
			this.redirectCheckbox.setDisabled( false );
		} else {
			this.redirectCheckbox.setSelected( true );
			this.redirectCheckbox.setDisabled( true );
		}
		if ( apiManager.currentUserCan( 'softblock' ) ) {
			this.softblockCheckbox.setSelected(
				apiManager.getPreference( 'softblock' )
			);
			this.redirectCheckbox.setDisabled( false );
		} else {
			this.softblockCheckbox.setSelected( false );
			this.softblockCheckbox.setDisabled( true );
		}

		// Run the event handlers once in case anyone has set strange
		// preferences.
		this.onSuggestrenameChange();
		if ( apiManager.currentUserCan( 'softblock' ) ) {
			this.onSoftblockChange();
		}
		this.onNotifyChange();
		this.onTitleChange();

		// Add event handlers
		this.softblockCheckbox.on( 'change', this.onSoftblockChange, null, this );
		this.suggestrenameCheckbox.on( 'change', this.onSuggestrenameChange, null, this );
		this.notifyCheckbox.on( 'change', this.onNotifyChange, null, this );
		this.titleInput.on( 'change', this.onTitleChange, null, this );

		// Mark as loaded
		this.isLoaded = true;
	};

	Dialog.prototype.onSuggestrenameChange = function () {
		if ( this.suggestrenameCheckbox.isSelected() ) {
			this.notifyCheckbox.setSelected( true );
			this.notifyCheckbox.setDisabled( true );
			this.softblockCheckbox.setSelected( false );
			this.softblockCheckbox.setDisabled( true );
		} else {
			if ( this.getApiManager().currentUserCan( 'softblock' ) ) {
				this.softblockCheckbox.setDisabled( false );
			}
			this.notifyCheckbox.setDisabled( false );
		}
		this.onNotifyChange();
	};

	Dialog.prototype.onSoftblockChange = function () {
		if ( this.softblockCheckbox.isSelected() ) {
			this.notifyCheckbox.setSelected( true );
			this.notifyCheckbox.setDisabled( true );
			this.suggestrenameCheckbox.setSelected( false );
			this.suggestrenameCheckbox.setDisabled( true );
		} else {
			this.suggestrenameCheckbox.setDisabled( false );
			this.notifyCheckbox.setDisabled( false );
		}
		this.onNotifyChange();
	};

	Dialog.prototype.onNotifyChange = function () {
		if ( this.notifyCheckbox.isSelected() ) {
			this.watchUserTalkCheckbox.setDisabled( false );
		} else {
			this.watchUserTalkCheckbox.setSelected( false );
			this.watchUserTalkCheckbox.setDisabled( true );
		}
	};

	Dialog.prototype.onTitleChange = function () {
		var self = this,
			promise = this.titleInput.getValidity();
		promise.done( function () {
			self.actions.setAbilities( { submit: true } );
		} );
		promise.fail( function () {
			self.actions.setAbilities( { submit: false } );
		} );
	};

	Dialog.prototype.onSubmit = function () {
		var options, promises, messages,
			self = this,
			apiManager = this.getApiManager(),
			targetUser = apiManager.getTargetUser();

		// Record that the dialog has been submitted. This is necessary to
		// prevent the "Submit" button from being reactivated after the window
		// is closed and reopened.
		self.hasBeenSubmitted = true;

		// Disable input
		self.actions.setAbilities( { submit: false } );

		options = {
			title: this.titleInput.getValue(),
			redirect: this.redirectCheckbox.isSelected(),
			notify: this.notifyCheckbox.isSelected(),
			suggestrename: this.suggestrenameCheckbox.isSelected(),
			softblock: this.softblockCheckbox.isSelected(),
			watchusertalk: this.watchUserTalkCheckbox.isSelected(),
			watchdraft: this.watchDraftCheckbox.isSelected()
		};

		promises = apiManager.submit( options );

		// Increase the pending level for each promise we have.
		$.each( promises, function ( key, value ) {
			if ( value ) {
				self.pushPending();
			}
		} );

		// Set the progress labels, hide them from view, and add them to the
		// fieldset.
		function addField( field, label, promise ) {
			if ( !promise ) {
				return;
			}
			field
				.setLabel( label )
				.setData( promise )
				.toggle();
			self.submitFieldset.addItems( [ field ] );
		}
		addField(
			self.moveProgressField,
			mw.message( 'dfy-move-progress-label' ).text(),
			promises.move
		);
		addField(
			self.tagProgressField,
			mw.message( 'dfy-tag-progress-label' ).text(),
			promises.tag
		);
		addField(
			self.softblockProgressField,
			mw.message( 'dfy-softblock-progress-label', targetUser ).text(),
			promises.softblock
		);
		addField(
			self.notifyProgressField,
			mw.message( 'dfy-notify-progress-label', targetUser ).text(),
			promises.notify
		);
		self.openTalkProgressField
			.setLabel( mw.message( 'dfy-open-talk-progress-label' ).text() )
			.toggle();
		self.submitFieldset.addItems( [ self.openTalkProgressField ] );

		// Update progress
		// This involves making the process fields visible, adding the progress
		// labels on success or failure, and reducing the pending level.
		self.moveProgressField.toggle();
		$.each( self.submitFieldset.getItems(), function ( i, field ) {
			var
				promise = field.getData(),
				label = field.getField();

			if ( !promise ) {
				return;
			}

			promise
				.done( function () {
					var nextField;

					label.setLabel( $( '<span>' )
						.addClass( 'draftify-success' )
						.text( mw.message( 'dfy-progress-success' ).text() )
					);

					// Only display the next field on success, as on error they
					// would all have the same error messages.
					nextField = self.submitFieldset.getItems()[ i + 1 ];
					if ( nextField ) {
						nextField.toggle();
					}
				} )
				.fail( function ( id, obj ) {
					if ( obj && obj.error && obj.error.info ) {
						label.setLabel( $( '<span>' )
							.addClass( 'draftify-error' )
							.text( mw.message(
								'dfy-progress-failed',
								obj.error.info
							).text() )
						);
					} else {
						label.setLabel(
							mw.message( 'dfy-progress-unknown-error' ).text()
						);
					}
				} )
				.always( function () {
					self.popPending();
				} );
		} );

		// Set final actions
		promises.all.done( function () {
			apiManager.openTalkPage();
		} );
		promises.all.fail( function () {
			// Pop pending only on failure, so that it still looks like we're
			// loading something while the talk page is being opened.
			self.popPending();
		} );

		// Switch to the panel view so that users can see what's going on.
		self.stackLayout.setItem( self.submitPanel );
	};

	Dialog.prototype.getActionProcess = function ( action ) {
		return Dialog.super.prototype.getActionProcess.call( this, action )
		.next( function () {
			if ( action === 'submit' ) {
				return this.onSubmit();
			} else {
				return Dialog.super.prototype.getActionProcess.call( this, action );
			}
		}, this );
	};

	/**************************************************************************
	 *                                Main
	 **************************************************************************/

	 function main() {
	 	// Load CSS
	 	importStylesheet( "User:Mr. Stradivarius/gadgets/Draftify.css" );
	 	
	 	// Set up objects
		var portletLink, windowManager,
			draftifyDialog = new Dialog( { size: 'medium' } ),
			apiManager = draftifyDialog.getApiManager();

		// Set up window manager
		windowManager = new OO.ui.WindowManager();
		$( 'body' ).append( windowManager.$element );
		windowManager.addWindows( [ draftifyDialog ] );

		// Add portlet link
		portletLink = apiManager.addPortletLink();
		$( portletLink ).click( function ( event ) {
			event.preventDefault();
			windowManager.openWindow( draftifyDialog );
		} );
	}

	main();
} );

// </nowiki>