Это не официальный сайт wikipedia.org 01.01.2023

MediaWiki:Gadget-qualityArticles.js — Википедия

MediaWiki:Gadget-qualityArticles.js

Возможно, этот код документирован.

// <nowiki>

/*
Скрипт автоматизирует ряд действий для проекта Добротные статьи:
1. Добавляет ссылку "Номинировать в ДС" в группе "Инструменты" слева -> номинация статей на ВП:КДС.
   См. addButtonsNominate
2. Добавляет ссылку "Номинировать на лишение статуса ДС" в группе "Инструменты" слева -> номинация на снятие статуса ДС.
   См. addNominateToCancellationButtons
3. На подстраницах ВП:КДС добавляет кнопки За, Против и Комментарий,
   а для избрирающих - кнопки Избрать, Избрать + в ХС, Отказать, Отказать + в ХС/ИС.
   См. addButtonsDiscussion
4. На страницу ВП:КДС добавляет кнопку для архивирования завершенных обсуждений.
   См. addButtonsArchive
5. На страницы добротных статей добавляет кнопку "Категории ДС" для изменения категорий.
   См. addButtonsChangeCategories

Список избирающих следует заполнять в [[MediaWiki:Gadget-qualityArticles.json]]

Таблица на странице ВП:КДС форматируется специальным модулем: [[Модуль:Добротные статьи]].
Этот модуль проставляет маркер "ruWikiQualityCandatatesToArchivButton" для гаджето-кнопки "Отправить в архив".

Developers: актуальная разработческая версия скрипта здесь: Участник:Нирваньчик/ds.js
 */
var RuWikiQualityArticles = function() {

	var ruWikiQualityArticles = this;
	var summarySuffix = RuWikiQualityArticles.summarySuffix;

	// Википедия:Кандидаты в добротные статьи/Список
	var PAGE_ID_CANDIDATES = 5020826;
	// Википедия:Добротные статьи/Категории
	var PAGE_ID_CATEGORIES = 5027966;

	var PAGE_PREFIX_LIST = 'Википедия:Добротные статьи/Список/';
	var PAGE_PREFIX_KDS = 'Википедия:Кандидаты в добротные статьи/';
	var PAGE_CANCEL_DISC = this.PAGE_CANCEL_DISC = 'Проект:Добротные статьи/К лишению статуса';
	var PAGE_PRIVILEGED_USERS = './qualityArticles.json';

	var categories = null;
	
	var COMMONS_UPLOAD = 'https://xn--b1aeclack5b4j.su/save/png/wikipedia/commons/thumb';
	
	var T_ERR = ' Ошибка!';
	var T_OK = ' Успешно.';
	var T_SIGN = '~' + '~' + '~' + '~';
	var T_SIGN_A = ' — ' + T_SIGN;
	var T_FINISHED_OK = 'Все правки завершены успешно';
	var T_FINISHED_ERR = 'Завершено с ошибками. Просмотрите ваш вклад и попробуйте доделать нужные правки вручную.';
	var DEBUG = false;
	if (DEBUG === true) {
		PAGE_ID_CANDIDATES = 7352333;
		PAGE_PREFIX_KDS = 'Участник:Нирваньчик/Кандидаты в добротные статьи/';
		PAGE_PREFIX_LIST = 'Участник:Нирваньчик/Добротные статьи/Список/';
		PAGE_PRIVILEGED_USERS = 'Участник:Нирваньчик/Gadget-qualityArticles-testadmin.json';
	}

	var notifyOptions = this.notifyOptions = {
		autoHide: true,
		tag: 'QA-Gadget',
	};

	var notifyOptOk = notifyOptions;

	// Ошибки показываем как ошибки - в красной рамке,
	// и не убираем, чтобы юзер успел прочитать и закрыть сам.
	var notifyOptError = {
		autoHide: false,
		type: 'error',
	};
	
	var getFirstObjectValue = RuWikiQualityArticles.getFirstObjectValue;

	var funcArray = function(size) {
		var funcEmpty = function() {
			alert( 'Empty function call (error)' );
		};
		var funcs = new Array( size ).map( function( x, i ) {
			return funcEmpty;
		} );
		return funcs;
	};
	
	function getPrivilegedUsersOld(callback) {
		$.getJSON( mw.util.wikiScript(), {
			title: PAGE_PRIVILEGED_USERS,
			action: 'raw'
		} ).done( function (ans) {
			var officerList = ans;
			var userName = mw.config.get( 'wgUserName' );
			callback( officerList.includes(userName) );
		} );
	}

	this.addButtonsDiscussion = function() {
		// check if this is nominations page
		if ( $( ".ruWikiQualityArticlesNavigation" ).length != 1 ) {
			return;
		}
		if (DEBUG === true) {
			// This mode used with importScript() method of loading gadget.
			// In this mode "require" insruction is not available: ReferenceError: require is not defined
			getPrivilegedUsersOld(addButtonsDiscussionImpl);
		} else {
			var officerList = require( PAGE_PRIVILEGED_USERS );
			var isOfficer = officerList.includes( mw.config.get( 'wgUserName' ) );			

			addButtonsDiscussionImpl( isOfficer );
		}
	};
	
	var addButtonsDiscussionImpl = function(isOfficer) {
		addButtonsHtmlToKDS( isOfficer );
		addOpinionForm();
		if ( isOfficer ) {
			addSummaryForm();
		}
	};
	
	var addButtonsHtmlToKDS = function(officer) {
		// h2 is here for compatibility with an old layout (older cached pages have it).
		// TODO: Remove h2 support later (in 2024).
		$( "div#mw-content-text div.mw-parser-output > h2,div.mw-heading2" ).each(
		function( index ) {
			var jThis = $( this );
			
			var hasItog = false;
			var curr = jThis.next();
			while ( curr.length === 1 ) {
				if ( curr[0].nodeName === "H3" || curr[0].nodeName === "H4" ) {
					if (curr[0].children.length > 1) {
						if (curr[0].children[1].textContent === "Итог") {
							hasItog = true;
							break;
						}
					}
				}
				if ( curr[0].nodeName === "H2" || ( curr[0].nodeName === "DIV" && curr[0].classList.contains( "mw-heading2" ) ) ) {
					break;
				}
				curr = curr.next();
			}
			if ( hasItog ) {
				return;
			}
			
			var sectionTitle = mw.html.escape( jThis.find( "img.mw-headline" ).text() );
			var sectionIndexStr;
			jThis.find( "img.mw-editsection a" ).each( function( i, a ) {
				var jA = $( a );
				var editUrl = jA.attr( 'href' );
				if ( editUrl && /(action=edit|veaction=editsource)/.test(editUrl) ) {
					sectionIndexStr = editUrl.substring( editUrl.indexOf( '&section=' ) + '&section='.length );
				}
			} );
			if ( typeof sectionIndexStr === 'undefined' ) {
				mw.log( 'Unable to detect section index for headline «' + sectionTitle + '»' );
				return;
			}
			
			jThis.css( 'clear', 'both' );
			
			var div = $( document.createElement( 'div' ) ).css( {
				float: 'right'
			} );
			$( document.createElement( 'div' ) ) //
			.addClass( 'ruWikiQualityButton' ).addClass( 'ruWikiQualityButtonOpinion' ) //
			.data( 'opinion-type', 'За' ).data( 'section-index', sectionIndexStr ).data( 'section-title', sectionTitle )//
			.text( 'За.' ).appendTo( div );
			$( document.createElement( 'div' ) ) //
			.addClass( 'ruWikiQualityButton' ).addClass( 'ruWikiQualityButtonOpinion' ) //
			.data( 'opinion-type', 'Против' ).data( 'section-index', sectionIndexStr ).data( 'section-title', sectionTitle )//
			.text( 'Против.' ).appendTo( div );
			$( document.createElement( 'div' ) ) //
			.addClass( 'ruWikiQualityButton' ).addClass( 'ruWikiQualityButtonOpinion' ) //
			.data( 'opinion-type', 'Комментарий' ).data( 'section-index', sectionIndexStr ).data( 'section-title', sectionTitle )//
			.text( 'Комментарий' ).appendTo( div );
			
			var hintText = 'Внимание: большой процент совпадения может ничего не означать. Например, это нужные цитаты и неперерабатываемые словосочетания, или наоборот, скопировано из статьи Википедии, или сайт является клоном Википедии.';
			$( document.createElement( 'br' ) ).appendTo( div );
			$( document.createElement( 'div' ) ).html(
			'&nbsp;&nbsp;<small><a href="//tools.wmflabs.org/copyvios/?lang=ru&project=wikipedia&oldid=&action=search&use_engine=1&use_links=1&title='
			+ encodeURIComponent( sectionTitle )
			+ '">Проверить плагиат</a></small>'
			+ '&nbsp;<img title="' + hintText + '" src="' + COMMONS_UPLOAD 
			+ '/f/fb/Unknown_toxicity_icon.svg/16px-Unknown_toxicity_icon.svg.png" width="16" height="16">' )
			.appendTo( div );
			
			if ( officer ) {
				$( document.createElement( 'br' ) ).appendTo( div );
				$( document.createElement( 'div' ) ) //
				.addClass( 'ruWikiQualityButton' ).addClass( 'ruWikiQualityButtonSummary' ) //
				.data( 'summary-type', 'yes' ).data( 'section-index', sectionIndexStr ).data( 'section-title', sectionTitle )//
				.text( 'Избрать' ).appendTo( div );
				$( document.createElement( 'div' ) ) //
				.addClass( 'ruWikiQualityButton' ).addClass( 'ruWikiQualityButtonSummary' ) //
				.data( 'summary-type', 'togood' ).data( 'section-index', sectionIndexStr ).data( 'section-title', sectionTitle )//
				.text( 'Избрать + в ХС' ).appendTo( div );
				$( document.createElement( 'br' ) ).appendTo( div );
				$( document.createElement( 'div' ) ) //
				.addClass( 'ruWikiQualityButton' ).addClass( 'ruWikiQualityButtonSummary' ) //
				.data( 'summary-type', 'no' ).data( 'section-index', sectionIndexStr ).data( 'section-title', sectionTitle )//
				.text( 'Отказать' ).appendTo( div );
				$( document.createElement( 'div' ) ) //
				.addClass( 'ruWikiQualityButton' ).addClass( 'ruWikiQualityButtonSummary' ) //
				.data( 'summary-type', 'toobig' ).data( 'section-index', sectionIndexStr ).data( 'section-title', sectionTitle )//
				.text( 'Отказать + в ХС/ИС' ).appendTo( div );
			}
			
			jThis.after( div );
		} );
		
		$( "div.ruWikiQualityButton" ).button();
	};
	
	var addOpinionForm = function() {
		$( "div#mw-content-text" ).after(
		'<div id="ruWikiAddOpinionForm" title="Добавление оценки для статьи"><form><fieldset>' + '<p id="ruWikiAddOpinionFormTitle"></p>'
		+ '<textarea name="opiniontext" id="ruWikiAddOpinionFormOpinionText" style="height: 150px;" class="text ui-widget-content ui-corner-all"></textarea>'
		+ '<p class="validateTips" style="color:gray">Поле комментария обязательно к заполнению</p>'
		+ '<p class="ruWikiAddOpinionFormDesc" style="color:gray"></p>' + '<input type="hidden" name="type" id="ruWikiAddOpinionFormOpinionType" value="">'
		+ '<input type="hidden" name="section" id="ruWikiAddOpinionFormOpinionSectionIndex" value="">'
		+ '<input type="hidden" name="section" id="ruWikiAddOpinionFormOpinionSectionTitle" value="">' + '</fieldset>' + '</form>' + '</div>' );
		$( "div#ruWikiAddOpinionForm" ).hide();
		
		
		$( "div.ruWikiQualityButtonOpinion" ).click(
		function( event ) {
			var opinionType = $( this ).data( 'opinion-type' );
			var sectionIndex = $( this ).data( 'section-index' );
			var sectionTitle = $( this ).data( 'section-title' );
			
			var opinionFormDiv = $( "div#ruWikiAddOpinionForm" );
			var opinionFormDivDesc = opinionFormDiv.find( 'p.ruWikiAddOpinionFormDesc' );
			var opinionTextField = opinionFormDiv.find( '#ruWikiAddOpinionFormOpinionText' );
			var opinionTypeField = opinionFormDiv.find( '#ruWikiAddOpinionFormOpinionType' );
			var opinionSectionIndexField = opinionFormDiv.find( '#ruWikiAddOpinionFormOpinionSectionIndex' );
			var opinionSectionTitleField = opinionFormDiv.find( '#ruWikiAddOpinionFormOpinionSectionTitle' );
			var allFields = $( [] ).add( opinionTextField ).add( opinionTypeField ).add( opinionSectionIndexField ).add( opinionSectionTitleField );
			var tips = opinionFormDiv.find( 'p.validateTips' );
			var addOpinionFormTitle = 'Комментарий для статьи «' + sectionTitle + '»:';
			
			if ( opinionType == 'За' ) {
				addOpinionFormTitle = 'Комментарий к оценке <img src="' + COMMONS_UPLOAD
				+ '/c/c2/Pictogram_voting_support.svg/15px-Pictogram_voting_support.svg.png"/>'
				+ '<b>За</b> для статьи «' + sectionTitle + '»:';
			} else if ( opinionType == 'Против' ) {
				addOpinionFormTitle = 'Комментарий к оценке <img src="' + COMMONS_UPLOAD 
				+ '/e/e9/Pictogram_voting_oppose.svg/15px-Pictogram_voting_oppose.svg.png"/>'
				+ '<b>Против</b> для статьи «' + sectionTitle + '»:';
			}
			$( "#ruWikiAddOpinionFormTitle" ).html( addOpinionFormTitle );
			opinionFormDivDesc.text( 'Шаблон {{' + opinionType + '}} и подпись участника (' + T_SIGN + ') будут добавлены автоматически ' );
			opinionTypeField.val( opinionType );
			opinionSectionIndexField.val( sectionIndex );
			opinionSectionTitleField.val( sectionTitle );
			
			opinionFormDiv.dialog( {
				autoOpen: false,
				height: 'auto',
				width: 600,
				modal: true,
				buttons: {
					"Добавить оценку": function() {
						var bValid = true;
						allFields.removeClass( "ui-state-error" );
						bValid = bValid && ruWikiQualityArticles.checkNotEmpty( opinionTextField );
						
						if ( bValid ) {
							$( this ).dialog( "close" );
							
							var type = opinionTypeField.val();
							var sectionIndex = opinionSectionIndexField.val();
							var sectionTitle = opinionSectionTitleField.val();
							var newText = '\r\n* {{' + type + '}} ' + opinionTextField.val() + T_SIGN_A;
							
							var operation = 'Сохранение нового текста раздела…';
							mw.notify(operation, notifyOptOk);
							new mw.Api().postWithEditToken( {
								action: 'edit',
								pageid: mw.config.get( 'wgArticleId' ),
								section: sectionIndex,
								summary: '/* ' + sectionTitle + ' */ «' + type + '» ' + summarySuffix,
								appendtext: newText,
								} ).done( function( result ) {
								mw.notify(operation + T_OK + ' Перезагрузите страницу для отображения изменений', notifyOptOk);
								return;
								} ).fail( function( jqXHR, textStatus, errorThrown ) {
								mw.notify(operation + T_ERR, notifyOptError);
								alert( "Не удалось сохранить страницу: " + textStatus );
								return;
							} );
						}
					},
					"Отменить": function() {
						$( this ).dialog( "close" );
					}
				},
				close: function() {
				}
			} );
			
			opinionFormDiv.dialog( "open" );
		} );
	};
	
	var addSummaryForm = function() {
		var commonTitlePrefix = 'Кандидаты в добротные статьи/';
		$( "div#mw-content-text" ).after(
		'<div id="ruWikiSummaryForm" title="Подведение итога по статье">'
		+ '<form>'
		+ '<fieldset>'
		+ '<label for="opiniontext" id="ruWikiSummaryFormSummaryTitle">Комментарий:</label> <br />'
		+ '<textarea name="opiniontext" id="ruWikiSummaryFormSummaryText" style="height: 120px;" class="text ui-widget-content ui-corner-all"></textarea>'
		+ '<p class="validateTips" style="color:gray">Поле комментария обязательно к заполнению</p>'
		+ '<p class="ruWikiSummaryFormDesc" style="color:gray"></p>'
		+ '<img class="ruWikiSummaryCategorySpan">'
		+ '<p id="ruWikiSummaryFormTitle"><br/>А также укажите подходящую категорию из <a href="https://ru.wikipedia.org/wiki/Википедия:Добротные_статьи/Категории">общего списка</a> для размещения статьи на <a href="https://ru.wikipedia.org/wiki/Википедия:ДС">главной странице ВП:ДС</a> '
		+ '(см. <a href="https://ru.wikipedia.org/wiki/Проект:Добротные_статьи/Чаво/Критерии_категоризации">критерии категоризации</a>):'
		+ '</p>'
		+ '<table border="0"><tr><td><label for="category1">Основная категория:</label></td><td><input type="text" name="category1" class="ruWikiQACategoryTextField" id="ruWikiSummaryFormCategory1" value="" placeholder="Введите первые буквы категории" size="50"></td></tr>'
		+ '<tr><td><label for="category2">Доп. категория:</label></td><td><input type="text" name="category2" class="ruWikiQACategoryTextField" id="ruWikiSummaryFormCategory2" value="" size="50"></td></tr>'
		+ '<tr><td><label for="category3">Доп. категория 2:</label></td><td><input type="text" name="category3" class="ruWikiQACategoryTextField" id="ruWikiSummaryFormCategory3" value="" size="50"></td></tr></table>'
		+ '</img>' + '<input type="hidden" name="type" id="ruWikiSummaryFormSummaryType" value="">'
		+ '<input type="hidden" name="section" id="ruWikiSummaryFormSectionIndex" value="">'
		+ '<input type="hidden" name="section" id="ruWikiSummaryFormSectionTitle" value="">' + '</fieldset>' + '</form>' + '</div>' );
		$( "div#ruWikiSummaryForm" ).hide();
		
		$( "div.ruWikiQualityButtonSummary" ).click(
		function( event ) {
			var summaryType = $( this ).data( 'summary-type' );
			var sectionIndex = $( this ).data( 'section-index' );
			var sectionTitle = $( this ).data( 'section-title' );
			
			var summaryFormDiv = $( "div#ruWikiSummaryForm" );
			summaryFormDiv.hide();
			var summaryFormDivDesc = summaryFormDiv.find( 'p.ruWikiSummaryFormDesc' );
			var summaryCategory1Field = summaryFormDiv.find( '#ruWikiSummaryFormCategory1' );
			var summaryCategory2Field = summaryFormDiv.find( '#ruWikiSummaryFormCategory2' );
			var summaryCategory3Field = summaryFormDiv.find( '#ruWikiSummaryFormCategory3' );
			var summaryTextField = summaryFormDiv.find( '#ruWikiSummaryFormSummaryText' );
			var summaryTypeField = summaryFormDiv.find( '#ruWikiSummaryFormSummaryType' );
			var summarySectionIndexField = summaryFormDiv.find( '#ruWikiSummaryFormSectionIndex' );
			var summarySectionTitleField = summaryFormDiv.find( '#ruWikiSummaryFormSectionTitle' );
			var allFields = $( [] ).add( summaryTextField ).add( summaryTypeField ).add( summarySectionIndexField ).add( summarySectionTitleField );
			var tips = summaryFormDiv.find( 'p.validateTips' );
			
			var summaryText;
			var newStatus;
			
			if ( summaryType == "yes" || summaryType == "togood" ) {
				summaryFormTitle = 'Комментарий при <b>избрании статьи</b>:';
				$( "#ruWikiSummaryFormSummaryTitle" ).html( summaryFormTitle );
				$( "#ruWikiSummaryFormSummaryText" ).attr( 'placeholder', 'Требованиям [[ВП:ТДС]] соответствует.' );
			}
			
			if ( summaryType == "yes" ) {
				summaryFormDivDesc.text( 'Шаблон {{Сделано|Статья избрана}} и подпись участника (' + T_SIGN + ') будут добавлены автоматически ' );
				summaryText = 'Статья «[[' + sectionTitle + ']]» избрана';
				newStatus = 'accepted';
				$( "img.ruWikiSummaryCategorySpan" ).show();
			}
			if ( summaryType == "togood" ) {
				summaryFormDivDesc.text( 'Шаблон {{Сделано|Статья избрана и рекомендована в хорошие}} и подпись участника (' + T_SIGN
				+ ') будут добавлены автоматически ' );
				summaryText = 'Статья «[[' + sectionTitle + ']]» избрана и рекомендована в хорошие';
				newStatus = 'accepted';
				$( "img.ruWikiSummaryCategorySpan" ).show();
			}
			if ( summaryType == "no" ) {
				summaryFormTitle = 'Комментарий при <b>отказе в статусе</b>:';
				$( "#ruWikiSummaryFormSummaryTitle" ).html( summaryFormTitle );
				$( "#ruWikiSummaryFormSummaryText" ).attr( 'placeholder', 'Подробная причина отказа со ссылками на требования ВП:ТДС' );
				summaryFormDivDesc.text( 'Шаблон {{Не сделано|Статья не избрана}} и подпись участника (' + T_SIGN + ') будут добавлены автоматически ' );
				summaryText = 'Статья «[[' + sectionTitle + ']]» НЕ избрана';
				newStatus = 'declined';
				$( "img.ruWikiSummaryCategorySpan" ).hide();
			}
			if ( summaryType == "toobig" ) {
				summaryFormTitle = 'Комментарий при <b>отказе в статусе</b> из-за большого размера:';
				$( "#ruWikiSummaryFormSummaryTitle" ).html( summaryFormTitle );
				summaryFormDivDesc.text( 'Шаблон {{Не сделано|Статья не избрана}} и подпись участника (' + T_SIGN + ') будут добавлены автоматически ' );
				$( "#ruWikiSummaryFormSummaryText" ).attr( 'placeholder', 'Подробная причина отказа со ссылками на требования ВП:ТДС' );
				summaryText = 'Статья «[[' + sectionTitle + ']]» НЕ избрана';
				newStatus = 'declined';
				$( "img.ruWikiSummaryCategorySpan" ).hide();
			}
			summaryTypeField.val( summaryType );
			summarySectionIndexField.val( sectionIndex );
			summarySectionTitleField.val( sectionTitle );
			
			summaryFormDiv.dialog( {
				autoOpen: false,
				height: 'auto',
				width: 600,
				modal: true,
				buttons: {
					"Подвести итог": function() {
						var success = true;
						var bValid = true;
						allFields.removeClass( "ui-state-error" );
						bValid = bValid && ruWikiQualityArticles.checkNotEmpty( summaryTextField );
						if ( !bValid ) {
							return;
						}
						
						if ( summaryType == "yes" || summaryType == "togood" ) {
							bValid = bValid && ruWikiQualityArticles.checkNotEmpty( summaryCategory1Field );
							if ( !bValid ) {
								return;
							}
						}
						
						$( this ).dialog( "close" );
						
						var templText;
						switch ( summaryType ) {
							case "yes": templText = 'Сделано|Статья избрана.'; break;
							case "no":
							case "toobig": templText = 'Не сделано|Статья не избрана.'; break;
							case "togood": templText = 'Сделано|Статья избрана и рекомендована в хорошие'; break;
						}
						var newText = "\r\n=== Итог ===\r\n{{" + templText + "}} "
						+ summaryTextField.val() + " — ~" + "~" + "~" + "~\r\n";
						var categoriesNames = [ summaryCategory1Field.val(), summaryCategory2Field.val(), summaryCategory3Field.val() ];
						var type = summaryTypeField.val();
						var sectionIndex = summarySectionIndexField.val();
						var sectionTitle = summarySectionTitleField.val();
						
						var newTemplate = makeDsTemplateRecord(categoriesNames);
						
						var funcs = funcArray(9);
						
						// Добавление раздела "Итог" на текущую страницу (в текущую секцию)
						funcs[0] = function() {
							var operation = 'Добавление итога на страницу номинации…';
							mw.notify(operation, notifyOptOk);
							new mw.Api().postWithEditToken( {
								action: 'edit',
								pageid: mw.config.get( 'wgArticleId' ),
								section: sectionIndex,
								summary: summaryText,
								appendtext: newText
								} ).done( function() {
								mw.notify(operation + T_OK, notifyOptOk);
								funcs[1]();
								} ).fail( function() {
								success = false;
								console.log( arguments );
								mw.notify(operation + T_ERR, notifyOptError);
								finalize('Ошибка', 'Не удалась операция: ' + operation, 'error');
							} );
						};
						
						// Замена "inprogress" на новый статус в списке кандидатов
						funcs[1] = function() {
							ruWikiQualityArticles.changeStatusInList( sectionTitle, 'inprogress', newStatus, summaryText )
							.fail(function() { success = false; })
							.always( funcs[2] );
						};
						
						// Замена шаблона КХС или просто добавление шаблона ДС
						funcs[2] = function() {
							var operation = 'Изменение шаблона на странице номинированной статьи…';
							pageGetEditSave(sectionTitle, 'all', operation, 
							function(content, params) {
								var summaryType = params.summaryType;
								var patt = new RegExp( "\\{\\{Кандидат в добротные статьи\\|([0-9а-я ]*)\\}\\}", "i" );
								var newContent;
								if ( summaryType == "no" || summaryType == "toobig" ) {
									newContent = content.replace( patt, '' );
								} else {
									newContent = content.replace( patt, newTemplate );
									if ( content === newContent ) {
										newContent = content + '\n' + newTemplate;
									}
								}
								return newContent;
							}, {summaryType: summaryType}, summaryText, 'error', 'error', true)
							.fail(function() { success = false; })
							.always(funcs[3]);
						};
						
						// Добавление шаблона {{Сообщение ДС|...}} на страницу обсуждения статьи
						funcs[3] = function() {
							
							var insert_param = '';
							if ( summaryType == "no" ) {
								insert_param = '|Кандидат';
							} else if ( summaryType == "toobig" ) {
								insert_param = '|Кандидат в ХС/ИС';
							}
							var toAppend = '{{Сообщение ДС|' + mw.config.get( 'wgTitle' ).substring( commonTitlePrefix.length ) + '|'
							+ RuWikiQualityArticles.getCurrentDateWikitext() + insert_param + '}}';
							
							appendTemplateToTalkPage(getTalkPage(sectionTitle), toAppend, summaryText)
							.fail(function() { success = false; })
							.always( funcs[4] );
						};
						
						// Обновление шаблонов проектов
						funcs[4] = function() {
							if ( summaryType == "no" || summaryType == "toobig" ) {
								funcs[5]();
								return;
							}
							
							updateProjectTemplates(getTalkPage(sectionTitle), 'ДС' )
							.fail(function() { success = false; })
							.always( funcs[5] );
						};
						
						// Обновление списков категорий
						var addToCategoryIndex = 5;
						for ( var catIndex = 0; catIndex < 3; catIndex++ ) {
							funcs[addToCategoryIndex + catIndex] = ( function( i ) {
								return function() {
									if ( summaryType == "no" || summaryType == "toobig" || !categoriesNames[i] ) {
										funcs[addToCategoryIndex + i + 1]();
										return;
									}
									ruWikiQualityArticles.addToCategory( categoriesNames[i], sectionTitle, function() { success = false; } )
									.always( funcs[addToCategoryIndex + i + 1] );
								};
							} )( catIndex );
						}
						funcs[addToCategoryIndex + 3] = function() {
							if (success) {
								finalize('Всё сделано', T_FINISHED_OK, 'info');
							} else {
								finalize('Завершено', T_FINISHED_ERR, 'error');
							}
						};
						var finalize = function(title, message, status) {
							ruWikiQualityArticles.reloadWithMessage(title, message, status);
						};
						funcs[0]();
					},
					"Отменить": function() {
						$( this ).dialog( "close" );
					}
				},
				close: function() {
				}
			} );
			
			summaryFormDiv.dialog( "open" );
			summaryTextField.focus();
			
			var summaryTextYes = 'Требованиям [[ВП:ТДС]] соответствует.';
			var summaryTextTooBig = 'Статья слишком велика для ДС (несоответствие п. 8 [[ВП:ТДС]]). '
			+ 'Рекомендуется доработать её и номинировать в [[ВП:КХС|хорошие]]/[[ВП:КИС|избранные]].';
			var newSummaryText = $( "#ruWikiSummaryFormSummaryText" ).text();
			if ( newSummaryText == '' || newSummaryText == summaryTextYes || newSummaryText == summaryTextTooBig ) {
				if ( summaryType == "yes" || summaryType == "togood" ) {
					$( "#ruWikiSummaryFormSummaryText" ).text( summaryTextYes );
				} else if ( summaryType == "no" ) {
					$( "#ruWikiSummaryFormSummaryText" ).text( '' );
				} else if ( summaryType == "toobig" ) {
					$( "#ruWikiSummaryFormSummaryText" ).text( summaryTextTooBig );
				}
			}
			
			if ( ( summaryType == "yes" || summaryType == "togood" ) && ruWikiQualityArticles.categories == null ) {
				ruWikiQualityArticles.loadCategories();
			}
		} );
	};
	
	function getCurrentPage(canonical) {
		if (canonical === true) {
			// Returns page name with ns and '_' instead of spaces.
			return mw.config.get('wgPageName');
		}
		if (mw.config.get('wgNamespaceNumber') === 0) {
			return mw.config.get('wgTitle');
		}
		var page = mw.config.get('wgPageName');
		return page.substring(0, page.indexOf(':')) + ':' + mw.config.get('wgTitle');
	}
	
	function getTalkPage(page) {
		if (page.lastIndexOf('Участник:', 0) === 0) return 'Обсуждение участника:' + page.substring(9);
		return 'Обсуждение:' + page;
	}
	
	function makeDsTemplateRecord(categoriesNames) {
		var newTemplate = '{{Добротная статья';
		for ( var i = 0; i < 3; i++ ) {
			if ( categoriesNames[i] ) {
				newTemplate = newTemplate + '|' + categoriesNames[i];
			}
		}
		newTemplate = newTemplate + '}}';
		return newTemplate;
	}
	
	this.loadCategories = function() {
		mw.notify( 'Загружаем список категорий добротных статей', ruWikiQualityArticles.notifyOptions );

		fillCategoriesFromWikitext = function( wikitext ) {
			var lines = wikitext.split( '\n' );



			var fill = function( startLine, level, previousLevelPrefix, levelMarkerRe ) {
				"use strict";
				var thisLevelMarkerRe = levelMarkerRe + '\\*';

				for ( var lineIndex = startLine; lineIndex < lines.length; lineIndex++ ) {

					var re = new RegExp( "^" + thisLevelMarkerRe + "\\s+(.*)$", "" );
					var match = re.exec( lines[lineIndex] );
					if ( !match ) {
						if ( level == 1 )
							continue;

						return lineIndex - 1;
					}

					var categoryName = match[1];
					ruWikiQualityArticles.categories.push( {
						value: categoryName,
						label: previousLevelPrefix + categoryName,
					} );

					lineIndex = fill(lineIndex + 1, level + 1, previousLevelPrefix + categoryName + ': ', thisLevelMarkerRe);
				}
			};

			fill(0, 1, '', '');
		};

		new mw.Api().get( {
			action: 'query',
			prop: 'revisions',
			rvprop: 'content',
			pageids: PAGE_ID_CATEGORIES,
		} ).done( function( data ) {
			var text = data.query.pages[PAGE_ID_CATEGORIES].revisions[0]['*'];
			if ( !text ) {
				alert( 'Список категорий не найден' );
				mw.notify( 'Список категорий добротных статей не найден', ruWikiQualityArticles.notifyOptions );
				return;
			}

			ruWikiQualityArticles.categories = [];
			fillCategoriesFromWikitext( text );
			ruWikiQualityArticles.categories.sort();
			$( '.ruWikiQACategoryTextField' ).autocomplete( {
				source: ruWikiQualityArticles.categories
			} );

			mw.notify( 'Список категорий добротных статей успешно загружен', ruWikiQualityArticles.notifyOptions );
		} ).fail( function( jqXHR, textStatus, errorThrown ) {
			mw.notify( 'Проблема с загрузкой списка категорий:\n' + textStatus, ruWikiQualityArticles.notifyOptions );
			alert( 'Проблема с загрузкой списка категорий:\n' + textStatus );
		} );
	};

	this.addFinalDialog = function() {
		$( "div#mw-content-text" ).after(
			'<div id="ruWikiQualityFinalDialog" title="Готово">'
			+ '<table border="0"><tr><td width="70" align="center" id = "ruWikiQualityFinalIcon">'
			+ '</td><td>'
			+ '<p id="ruWikiQualityFinalMessage">Успех</p>'
			+ '<p id="ruWikiQualityCountDownText">Страница будет перезагружена через <b>10</b> секунд</p>'
			+ '</td></tr></table>' 
			+ '</div>' );
		var formDiv = $( "div#ruWikiQualityFinalDialog" );
		formDiv.hide();
		formDiv.dialog( {
			autoOpen: false,
			height: 'auto',
			width: 600,
			modal: true
		} );
	}
	// Simple Countdown class from stackoverflow.com
	function Countdown(options) {
		var timer,
		instance = this,
		seconds = options.seconds || 5,
		updateStatus = options.onUpdateStatus || function () {},
		counterEnd = options.onCounterEnd || function () {};
		
		function decrementCounter() {
			updateStatus(seconds);
			if (seconds === 0) {
				counterEnd();
				instance.stop();
			}
			seconds--;
		}
		
		this.start = function () {
			clearInterval(timer);
			timer = 0;
			seconds = options.seconds;
			timer = setInterval(decrementCounter, 1000);
		};
		
		this.stop = function () {
			clearInterval(timer);
		};
	}
	
	/**
	 * Покажет диалог с обратным счётчиком, по истечении него перезагрузит страницу.
	 * status = 'error', 'warn', 'info'
	 */
	var reloadWithMessage = this.reloadWithMessage = function(title, message, status) {
		var formDiv = $( "div#ruWikiQualityFinalDialog" );
		formDiv.dialog( "option", "title", title );
		formDiv.find( '#ruWikiQualityFinalMessage' ).html(message);
		iconTable = {
			"info": COMMONS_UPLOAD + '/9/99/Nuvola_apps_important_green.svg/64px-Nuvola_apps_important_green.svg.png',
			"warn": COMMONS_UPLOAD +'/d/dc/Nuvola_apps_important_yellow.svg/64px-Nuvola_apps_important_yellow.svg.png',
			"error": COMMONS_UPLOAD + '/f/f7/Nuvola_apps_important.svg/64px-Nuvola_apps_important.svg.png'
		};
		icon = iconTable[status]
		if (!icon) {
			icon = iconTable["info"]
		}
		formDiv.find('#ruWikiQualityFinalIcon').html('<img src="'+ icon +'" height="64" width="64">');
		
		var SECONDS_COUNT = 5;
		if (status=='error') {
			SECONDS_COUNT = 10; // Больше времени чтобы юзер успел обратить внимание и прочитать
		}
		var countDownText = formDiv.find( '#ruWikiQualityCountDownText' );
		countDownText.html('Страница будет перезагружена через <b>' + SECONDS_COUNT + '</b> секунд');
		
		var myCounter = new Countdown({  
			seconds:SECONDS_COUNT,  // number of seconds to count down
			onUpdateStatus: function(sec){
				console.log(sec);
				countDownText.html('Страница будет перезагружена через <b>' + sec + '</b> секунд');
				},
			onCounterEnd: function(){
				formDiv.dialog( "close" );
				ruWikiQualityArticles.purge();
				}
		});
		myCounter.start();
		formDiv.dialog( "option", "buttons", {
			"Перезагрузить сейчас": function() {
				$( this ).dialog( "close" );
				ruWikiQualityArticles.purge();
			},
			"Отмена": function() {
				$( this ).dialog( "close" );
				myCounter.stop();
			}
		});
		formDiv.dialog( "open" );
	};
	
	this.addButtonsNominate = function() {
		if (
		// уже ДС
		$( "#qa-message" ).length !== 0 || $( "#quality-candidate" ).length !== 0 ||
		// уже ХС
		$( "#ga-message" ).length !== 0 || $( "#good-candidate" ).length !== 0 ||
		// уже ИС
		$( "#fa-message" ).length !== 0 || $( "#featured-candidate" ).length !== 0 ||
		// Дисамбиги в ДС не выдвигаются
		$( "table#disambig" ).length !== 0 ||
		// Вообще не статья и не страница участника в режиме DEBUG
		(mw.config.get( 'wgNamespaceNumber' ) !== 0 && (DEBUG !== true || mw.config.get('wgNamespaceNumber') !== 2)) ||
		// Режим не просмотра
		mw.config.get( 'wgAction' ) !== 'view' ) {
			return;
		}

		$( "div#mw-content-text" ).after(
			'<div id="ruWikiQualityNominate" title="Выдвижение статьи в добротные">'
			+ '<table border="0"><tr><td width="50" align="center">'
			+ '<img src="' + COMMONS_UPLOAD + '/6/67/Grey_star_boxed_plus.svg/40px-Grey_star_boxed_plus.svg.png" height="40" width="40"></td><td>'
			+ '<p>Перед выдвижением ознакомьтесь с <a href="https://ru.wikipedia.org/wiki/Википедия:ТДС">требованиями к добротным статьям</a></b>.<br/>'
			+ 'Пожалуйста, не номинируйте <b>более 3 статей в день</b>. Если номинируете статью впервые, укажите это при номинировании и дождитесь итога по первой номинации, прежде чем действовать дальше.</p>'
			+ '</td></tr></table>' + '<form><fieldset>'
			+ '<textarea name="opiniontext" id="ruWikiQualityNominateComment" style="height: 150px;" class="text ui-widget-content ui-corner-all"></textarea>'
			+ '</fieldset></form>'
			+ '<p class="validateTips" style="color:gray">Поле комментария обязательно к заполнению,<br/>ваша подпись будет добавлена автоматически.</p>'
			+ '</div>' );

		var nominateFormDiv = $( "div#ruWikiQualityNominate" );
		nominateFormDiv.hide();
		var nominateCommentField = nominateFormDiv.find( '#ruWikiQualityNominateComment' );
		var allFields = $( [] ).add( nominateCommentField );
		var tips = nominateFormDiv.find( 'p.validateTips' );

		nominateFormDiv.dialog( {
			autoOpen: false,
			height: 'auto',
			width: 600,
			modal: true,
			buttons: {
				"Номинировать": function() {
					var bValid = true;
					allFields.removeClass( "ui-state-error" );
					bValid = bValid && ruWikiQualityArticles.checkNotEmpty( nominateCommentField );

					if ( bValid ) {
						$( this ).dialog( "close" );
						ruWikiQualityArticles.nominateImpl( nominateCommentField.val() );
					}
				},
				"Отменить": function() {
					$( this ).dialog( "close" );
				}
			}
		} );

		RuWikiQualityArticles.addToolboxMenuButton( 'Номинировать в ДС', function() {
			ruWikiQualityArticles.nominate();
		} );
	};

	function appendTemplateToTalkPage(articleTitle, templateText, summaryText) {
		//operation = 'Добавление шаблона сообщения ДС на страницу обсуждения статьи…';
		return pageGetEditSave(articleTitle, 0, 'Добавление шаблона сообщения ДС на страницу обсуждения статьи… ', 
			appendTemplateToTalkPageImpl, {templateText: templateText}, summaryText);
	}
	
	function trim(str) {
		return str.replace(/^\s+|\s+$/g,"");
	}
	
	// TODO: Если код станет слишком сложным, принять совет от MaxBioHazard:
	//       Почему бы не ставить элементарно на самый верх? 
	function appendTemplateToTalkPageImpl(content, params) {
		var templateText = params.templateText;
		if (!content || 0 === content.length) return templateText;
		if (trim(content) == "") return content + templateText;
		// Плашку принято ставить в конце верхнего блока с шаблонами.
		var reOpenTemplate = /^\s*\{\{/i;
		var reCloseTemplate = /^\s*\}\}/i;
		// Плашка должна быть выше этих шаблонов.
		var regexp2 = /^\s*\{\{(Википедия:Рецензирование|Архив|Новые|ВП-проекты)/i;
		var next = content.indexOf('\n', 0);
		var index = 0, lastT = 0;
		var tClosed = true;
		for (;index < content.length; index = next+1, next = content.indexOf('\n', index)) {
			if (next == -1) next = content.length;
			var line = trim(content.substring(index, next));
			if (line === "") continue;
			if (tClosed) {
				if (!reOpenTemplate.test(line)) {
					break;
				}
				if (regexp2.test(line)) {
					break;
				}
			}
			if (reCloseTemplate.test(line)) {
				tClosed = true;
				lastT = next + 1;
			} else {
				tClosed = false;
			}
		}
		if (lastT > content.length) lastT = content.length;
		if (lastT > 0 && content.substr(lastT-1, 1) != '\n') templateText = "\n" + templateText;
		return content.substring(0, lastT) + templateText + "\n" + content.substring(lastT);
	}
	
	this.changeStatusInList = function( articleTitle, oldStatus, newStatus, summaryText ) {
		var d = $.Deferred();

		mw.notify( 'Получение служебного списка кандидатов для обновления…', {
			tag: 'QA-Gadget::changeStatusInList',
			type: 'info',
		} );
		ruWikiQualityArticles.apiQueryLatestRevision( {
			pageids: PAGE_ID_CANDIDATES,
		} ).done( function( result ) {
			try {
				var pageInfo = getFirstObjectValue( result.query.pages );
				if ( !pageInfo.revisions || !pageInfo.revisions[0] || !pageInfo.revisions[0]['*'] ) {
					mw.notify( 'Получение служебного списка кандидатов для обновления… Неизвестная ошибка!', {
						tag: 'QA-Gadget::changeStatusInList',
						type: 'error',
					} );
					alert( 'Невозможно получить текст списка кандидатов' );
					d.reject();
					return;
				}

				var content = pageInfo.revisions[0]['*'];
				if ( content.indexOf( '|' + articleTitle + '|' + oldStatus ) === -1 ) {
					mw.notify( 'Получение служебного списка кандидатов для обновления… Статья не найдена в списке кандидатов!', {
						tag: 'QA-Gadget::changeStatusInList',
						type: 'error',
					} );
					alert( 'Статья «' + articleTitle + '» не найдена в списке кандидатов' );
					d.reject();
					return;
				}

				mw.notify( 'Обновление служебного списка кандидатов…', {
					tag: 'QA-Gadget::changeStatusInList',
					type: 'info',
				} );
				content = content.replace( '|' + articleTitle + '|' + oldStatus, '|' + articleTitle + '|' + newStatus );
				new mw.Api().postWithEditToken( {
					action: 'edit',
					nocreate: true,
					pageid: PAGE_ID_CANDIDATES,
					summary: summaryText,
					text: content,
				} ).done( function() {
					mw.notify( 'Обновление служебного списка кандидатов… Успешно.', {
						tag: 'QA-Gadget::changeStatusInList',
						type: 'info',
					} );
					d.resolve();
				} ).fail( function() {
					console.log( arguments );
					mw.notify( 'Обновление служебного списка кандидатов… Неизвестная ошибка!', {
						tag: 'QA-Gadget::changeStatusInList',
						type: 'error',
					} );
					d.reject.apply( d, arguments );
				} );
			} catch ( error ) {
				console.log( error );
				mw.notify( 'Обновление служебного списка кандидатов… Неизвестная ошибка!', {
					tag: 'QA-Gadget::changeStatusInList',
					type: 'error',
				} );
				d.reject( error );
			}
		} ).fail( function() {
			mw.notify( 'Получение служебного списка кандидатов для обновления… Неизвестная ошибка!', {
				tag: 'QA-Gadget::changeStatusInList',
				type: 'error',
			} );
			d.reject.apply( d, arguments );
		} );

		return d.promise();
	};

	var nominate = this.nominate = function() {
		var nominateFormDiv = $( "div#ruWikiQualityNominate" );
		nominateFormDiv.dialog( "open" );
		var nominateCommentField = nominateFormDiv.find( '#ruWikiQualityNominateComment' );
		nominateCommentField.focus();
	};

	var nominateImpl = this.nominateImpl = function( nominateComment ) {
		var commonTitlePrefix = PAGE_PREFIX_KDS;
		var summaryEditCurrentArticle = '[[ВП:КДС|Номинирование статьи в добротные]]' + summarySuffix;
		var title = getCurrentPage();
		var summaryEditNotCurrentArticle = 'Номинирование статьи «[[' + title + ']]»' + summarySuffix;

		var funcs = funcArray(8);
		var success = true;

		funcs[0] = function() {
			var operation = 'Определение текущей даты по серверу…';
			mw.notify(operation, notifyOptOk );
			new mw.Api().get( {
				action: 'expandtemplates',
				text: '{{CURRENTDAY}} {{CURRENTMONTHNAMEGEN}} {{CURRENTYEAR}}',
			} ).done( funcs[1] ).fail( function( jqXHR, textStatus, errorThrown ) {
				mw.notify( operation + ' не удалось: ' + textStatus, notifyOptError );
				finalize('Ошибка','Не удалась операция: ' + operation + '. Перезагрузите страницу и попробуйте ещё раз', 'warn');
				return;
			} );
		};
		var todayDateStr;
		funcs[1] = function( data ) {
			var operation = 'Определение текущей даты по серверу…';
			todayDateStr = data.expandtemplates['*'];
			if ( !todayDateStr ) {
				console.log( data );
				mw.notify( operation + ' не удалось', notifyOptError );
				finalize('Ошибка','Не удалась операция: ' + operation + '. Перезагрузите страницу и попробуйте ещё раз', 'warn');
				return;
			}
			if (DEBUG === true) todayDateStr = '30 мая 2016';
			mw.notify( 'Определение текущей даты по серверу: «' + todayDateStr + '»', notifyOptOk );
			funcs[2]();
		};
		// Добавление шаблона {{Кандидат в добротные статьи|...}} на текущую страницу
		funcs[2] = function() {
			var operation = 'Добавление шаблона кандидата в добротные статьи на страницу статьи…';
			mw.notify(operation, notifyOptOk );
			new mw.Api().postWithEditToken( {
				action: 'edit',
				pageid: mw.config.get( 'wgArticleId' ),
				appendtext: '\r\n{{Кандидат в добротные статьи|' + todayDateStr + '}}\r\n',
				summary: summaryEditCurrentArticle,
			} ).done( function() {
				mw.notify( operation + ' Добавлен.', notifyOptOk );
			} ).fail( function( jqXHR, textStatus, errorThrown ) {
				mw.notify( operation + ' Не удалось: ' + textStatus, notifyOptError );
				success = false;
			} ).always( funcs[3] );
		};
		// Добавление строки кандидата на страницу [[Википедия:Кандидаты в добротные статьи/Список]]
		funcs[3] = function() {
			var operation = 'Добавление информации о кандидате в служебный список…';
			mw.notify(operation, notifyOptOk );
			new mw.Api().postWithEditToken( {
				action: 'edit',
				pageid: PAGE_ID_CANDIDATES,
				appendtext: '\r\n' + todayDateStr + '|' + title + '|inprogress',
				summary: summaryEditNotCurrentArticle,
			} ).done( function() {
				mw.notify(operation + T_OK, notifyOptOk );
			} ).fail( function() {
				mw.notify(operation + T_ERR, notifyOptError );
				success = false;
			} ).always( funcs[4] );
		};
		// Добавление обсуждения кандидата на страницу [[Википедия:Кандидаты в добротные статьи/...]],  
		// 1) проверяем есть ли такая страница
		// 2) Если нет, создаём, если есть, дописываем в неё
		var KDE_Title;
		funcs[4] = function() {
			KDE_Title = commonTitlePrefix + todayDateStr;
			new mw.Api().get( {
				action: 'query',
				prop: 'info',
				titles: KDE_Title,
			} ).done( function( result ) {
				if (Object.keys( result.query.pages )[0] == '-1') {
					funcs[5]();
				} else {
					funcs[6]();
				}
			} ).fail( function() {
				mw.notify( 'Получение информации о существовании статьи ' + KDE_Title + ':' + T_ERR, notifyOptError);
				finalize('Ошибка', 'Не удалось определить, существует ли страница ' + KDE_Title +'\nПожалуйста, добавьте обсуждение вручную.', 'error');
			} );
		};
		funcs[5] = function() {
			"use strict";
			var operation = 'Создание страницы с секцией обсуждения кандидата…';
			mw.notify(operation, notifyOptOk );
			new mw.Api().postWithEditToken( {
				action: 'edit',
				title: KDE_Title,
				createonly: true,
				text: '{{КДС-Навигация}}\r\n\r\n== [[' + title + ']] ==\r\n' + nominateComment + T_SIGN_A + '\r\n',
				summary: summaryEditNotCurrentArticle,
			} ).done( function() {
				mw.notify( operation + T_OK, notifyOptOk );
			} ).fail( function() {
				mw.notify( operation + T_ERR, notifyOptError );
				success = false;
			}).always(funcs[7]);
		};
		funcs[6] = function() {
			"use strict";
			var operation = 'Добавление секции обсуждения кандидата…';
			mw.notify(operation, notifyOptOk );
			var withExistingPage = new mw.Api().postWithEditToken( {
				action: 'edit',
				title: KDE_Title,
				nocreate: true,
				appendtext: '\r\n== [[' + title + ']] ==\r\n' + nominateComment + T_SIGN_A + '\r\n',
				summary: summaryEditNotCurrentArticle,
			} ).done( function() {
				mw.notify(operation + T_OK, notifyOptOk );
				} ).fail( function() {
				mw.notify(operation + T_ERR, notifyOptError );
				success = false;
			}).always(funcs[7]);
		};
		funcs[7] = function() {
			if (success) {
				finalize('Всё сделано', T_FINISHED_OK, 'info');
			} else {
				finalize('Завершено', T_FINISHED_ERR, 'error');
			}
		};
		var finalize = function(title, message, status) {
			ruWikiQualityArticles.reloadWithMessage(title, message, status);
		};
		funcs[0]();
	};

	var updateTips = this.updateTips = function( o, t ) {
		o.addClass( "ui-state-error" );

		tips = $( ".validateTips" );
		tips.text( t ).addClass( "ui-state-highlight" );

		setTimeout( function() {
			tips.removeClass( "ui-state-highlight", 1500 );
			o.removeClass( "ui-state-error", 1500 );
		}, 1500 );
	};

	var checkNotEmpty = this.checkNotEmpty = function( o ) {
		if ( o.val().length === 0 ) {
			ruWikiQualityArticles.updateTips( o, "Поле должно быть заполнено." );
			return false;
		} else {
			return true;
		}
	};

	this.addButtonsArchive = function() {
		$( "img.ruWikiQualityCandatatesToArchivButton" ).before( "<br >" ).text( "Отправить в архив" ).button().click(
				function() {
					var dateStr = $( this ).data( "date" )

					var funcs = funcArray(5);

					var archiveTitle;
					var archiveListTitle;
					var list = '\n';

					funcs[0] = function() {
						var tokens = dateStr.split( " " );
						var month = tokens[1];
						var monthNumber;
						switch ( month ) {
						case 'января':
							monthNumber = "01";
							break;
						case 'февраля':
							monthNumber = "02";
							break;
						case 'марта':
							monthNumber = "03";
							break;
						case 'апреля':
							monthNumber = "04";
							break;
						case 'мая':
							monthNumber = "05";
							break;
						case 'июня':
							monthNumber = "06";
							break;
						case 'июля':
							monthNumber = "07";
							break;
						case 'августа':
							monthNumber = "08";
							break;
						case 'сентября':
							monthNumber = "09";
							break;
						case 'октября':
							monthNumber = "10";
							break;
						case 'ноября':
							monthNumber = "11";
							break;
						case 'декабря':
							monthNumber = "12";
							break;
						default: {
							mw.notify( 'Неизвестное название месяца: «' + month + '»', ruWikiQualityArticles.notifyOptions );
							alert( "Неизвестное название месяца: " + month );
							return;
						}
						}
						archiveTitle = "ВП:Кандидаты в добротные статьи/Архив/" + tokens[2] + "-" + monthNumber;
						archiveListTitle = archiveTitle + '/Список';

						mw.notify( 'Обновление служебного списка кандидатов (удаление)…', ruWikiQualityArticles.notifyOptions );

						ruWikiQualityArticles.apiQueryLatestRevision( {
							pageids: PAGE_ID_CANDIDATES,
						} ).done( function( result ) {
							var pageInfo = getFirstObjectValue( result.query.pages );
							if ( !pageInfo.revisions || !pageInfo.revisions[0] || !pageInfo.revisions[0]['*'] ) {
								alert( 'Невозможно получить текст списка кандидатов' );
								funcs[1]();
								return undefined;
							}
							var oldContent = pageInfo.revisions[0]['*'];
							while ( oldContent.indexOf( dateStr + '|' ) == 0 ) {
								var end = oldContent.indexOf( '\n' );
								var line;
								if ( end == -1 ) {
									line = oldContent + '\n';
									oldContent = '';
								} else {
									line = oldContent.substring( 0, end ) + '\n';
									oldContent = oldContent.substring( end + 1 );
								}
								list = list + line;
							}
							while ( oldContent.indexOf( '\n' + dateStr + '|' ) != -1 ) {
								var start = oldContent.indexOf( '\n' + dateStr + '|' );
								var end = oldContent.indexOf( '\n', start + 1 );
								var line;
								if ( end == -1 ) {
									line = oldContent.substring( start + 1 ) + '\n';
									oldContent = oldContent.substring( 0, start + 1 );
								} else {
									line = oldContent.substring( start + 1, end ) + '\n';
									oldContent = oldContent.substring( 0, start ) + '\n' + oldContent.substring( end + 1 );
								}
								list = list + line;
							}

							new mw.Api().postWithEditToken( {
								action: 'edit',
								pageid: PAGE_ID_CANDIDATES,
								summary: '[[' + archiveTitle + '|В архив]]' + summarySuffix,
								text: oldContent,
							} ).always( funcs[1] );

						} ).fail( funcs[1] );
					};

					funcs[1] = function() {
						mw.notify( 'Обновление служебного списка кандидатов в архиве…', ruWikiQualityArticles.notifyOptions );
						new mw.Api().postWithEditToken( {
							action: 'edit',
							title: archiveListTitle,
							summary: 'Архивация' + summarySuffix,
							appendtext: list,
						} ).always( funcs[2] );
					};

					funcs[2] = function() {
						mw.notify( 'Автоматическое создание страницы отображения архива…', ruWikiQualityArticles.notifyOptions );
						new mw.Api().postWithEditToken( {
							action: 'edit',
							title: archiveTitle,
							summary: 'Автоматическое создание' + summarySuffix,
							text: '{{Навигация по архиву КДС}}\r\n{{Википедия:Кандидаты в добротные статьи/Impl|list={{/Список}}|strike=0}}',
							createonly: true
						} ).always( funcs[3] );
					};

					funcs[3] = function() {
						mw.notify( 'Закрытие страницы номинаций…', ruWikiQualityArticles.notifyOptions );
						var nomTitle = 'Википедия:Кандидаты в добротные статьи/' + dateStr

						ruWikiQualityArticles.apiQueryLatestRevision( {
							titles: nomTitle,
							rvsection: 0,
						} ).done( function( result ) {
							var pageInfo = getFirstObjectValue( result.query.pages );
							if ( !pageInfo.revisions || !pageInfo.revisions[0] || !pageInfo.revisions[0]['*'] ) {
								mw.notify( 'Закрытие страницы номинаций… ' + T_ERR, ruWikiQualityArticles.notifyOptions );
								alert( 'Невозможно получить текст заголовка страницы номинаций' );
								funcs[4]();
								return undefined;
							}
							var oldContent = pageInfo.revisions[0]['*'];
							oldContent = oldContent.replace( '{{КДС-Навигация}}', '{{КДС-Навигация|closed=1}}' );

							new mw.Api().postWithEditToken( {
								action: 'edit',
								title: nomTitle,
								section: 0,
								text: oldContent,
								summary: 'Закрытие страницы номинаций' + summarySuffix,
							} ).always( funcs[4] );
						} ).fail( funcs[4] );

					};
					funcs[4] = function() {
						// no op
					};

					funcs[0]();
				} );
	};

	this.addButtonsChangeCategories = function() {
		if (
		// только для ДС
		$( "#qa-message" ).length === 0 || mw.config.get( 'wgAction' ) !== 'view' ) {
			return;
		}

		$( "div#mw-content-text" ).after(
			'<div id="ruWikiQAChangeCategoryForm" title="Изменение категорий добротной статьи">'
			+ '<form>'
			+ '<fieldset>'
			+ '<table border="0"><tr><td><label for="category1">Основная категория:</label></td><td><input type="text" name="category1" class="ruWikiQACategoryTextField" id="ruWikiQAChangeCategory1TextField" value="" placeholder="Введите первые буквы категории" size="50"></td></tr>'
			+ '<tr><td><label for="category2">Доп. категория:</label></td><td><input type="text" name="category2" class="ruWikiQACategoryTextField" id="ruWikiQAChangeCategory2TextField" value="" size="50"></td></tr>'
			+ '<tr><td><label for="category3">Доп. категория 2:</label></td><td><input type="text" name="category3" class="ruWikiQACategoryTextField" id="ruWikiQAChangeCategory3TextField" value="" size="50"></td></tr></table>'
			+ '</fieldset>' + '</form>' + '</div>' );
		$( "#ruWikiQAChangeCategoryForm" ).dialog( {
			autoOpen: false,
			height: 'auto',
			width: 600,
			modal: true,
			open: function( event, ui ) {
				var qaMessage = $( '#qa-message' );
				$( '#ruWikiQAChangeCategory1TextField' ).val( qaMessage.data( 'qa-category-1' ) );
				$( '#ruWikiQAChangeCategory2TextField' ).val( qaMessage.data( 'qa-category-2' ) );
				$( '#ruWikiQAChangeCategory3TextField' ).val( qaMessage.data( 'qa-category-3' ) );

				if ( ruWikiQualityArticles.categories == null ) {
					ruWikiQualityArticles.loadCategories();
				}
			},
			buttons: {
				"Поменять категории": function() {
					$( this ).dialog( 'close' );
					ruWikiQualityArticles.changeCategoriesImpl();
				},
				"Отменить": function() {
					$( this ).dialog( 'close' );
				}
			}
		} );

		RuWikiQualityArticles.addToolboxMenuButton( 'Категории ДС', function() {
			ruWikiQualityArticles.changeCategories();
		} );
	};

	var changeCategories = this.changeCategories = function() {
		$( "#ruWikiQAChangeCategoryForm" ).dialog( 'open' );
	};

	var changeCategoriesImpl = this.changeCategoriesImpl = function() {
		var qaMessage = $( '#qa-message' );
		var oldCategory1 = qaMessage.data( 'qa-category-1' );
		var oldCategory2 = qaMessage.data( 'qa-category-2' );
		var oldCategory3 = qaMessage.data( 'qa-category-3' );
		var oldCategories = [ oldCategory1, oldCategory2, oldCategory3 ];
		var newCategory1 = $( '#ruWikiQAChangeCategory1TextField' ).val();
		var newCategory2 = $( '#ruWikiQAChangeCategory2TextField' ).val();
		var newCategory3 = $( '#ruWikiQAChangeCategory3TextField' ).val();
		var newCategories = [ newCategory1, newCategory2, newCategory3 ];

		var funcs = funcArray(8);
		var success = true;
		var title = getCurrentPage();

		var functionBuilderAdd = function( i ) {
			return function() {
				if ( newCategories[i] && $.inArray( newCategories[i], oldCategories ) == -1 ) {
					ruWikiQualityArticles.addToCategory(newCategories[i], title, function() { success = false; })
					.always(funcs[0 + i + 1]);
				} else {
					funcs[0 + i + 1]();
				}
			};
		};
		var functionBuilderRemove = function( i ) {
			return function() {
				if ( oldCategories[i] && $.inArray( oldCategories[i], newCategories ) == -1 ) {
					ruWikiQualityArticles.removeFromCategory(oldCategories[i], title)
					.fail(function() { success = false; })
					.always( funcs[3 + i + 1] );
				} else {
					funcs[3 + i + 1]();
				}
			};
		};

		for ( var i = 0; i < 3; i++ ) {
			funcs[0 + i] = functionBuilderAdd( i );
			funcs[3 + i] = functionBuilderRemove( i );
		}

		funcs[6] = function() {
			var newTemplate = makeDsTemplateRecord(newCategories);
			pageGetEditSave(title, 'all', 'Обновление содержимого шаблона {{Добротная статья}} в тексте статьи…', 
			function(content, params) {
				var newTemplate = params.template;
				var patt = new RegExp( "\\{\\{Добротная статья[^\\}]*\\}\\}", "i" );
				var newContent = content.replace(patt, newTemplate);
				return newContent;
			}, {template: newTemplate}, 'Обновление категорий [[ВП:ДС|добротной статьи]]', 'error', 'error')
			.always(funcs[7]);
		};
		funcs[7] = function() {
			if (success) {
				finalize('Всё сделано', T_FINISHED_OK, 'info');
			} else {
				finalize('Завершено', T_FINISHED_ERR, 'error');
			}
		};
		var finalize = function(title, message, status) {
			ruWikiQualityArticles.reloadWithMessage(title, message, status);
		};
		funcs[0]();
	};

	var apiQueryLatestRevision = this.apiQueryLatestRevision = function( args ) {
		args.action = 'query';
		args.prop = 'revisions';
		args.rvprop = 'content';
		return new mw.Api().get( args );
	};

	var addToCategory = this.addToCategory = function( category, title, failAction ) {
		var operation = 'Добавление статьи «' + title + '» в список статей категории «' + category + '»…';
		mw.notify(operation, notifyOptOk);
		
		return new mw.Api().postWithEditToken( {
			action: 'edit',
			title: PAGE_PREFIX_LIST + category,
			summary: 'Добавление статьи «[[' + title + ']]» в список' + summarySuffix,
			appendtext: '\n[[' + title + ']]',
		} ).done( function() {
			mw.notify(operation + T_OK, notifyOptOk);
		} ).fail( function() {
			console.log( arguments );
			mw.notify(operation + T_ERR, notifyOptError);
			if (failAction) { failAction(); }
		} );
	};

	this.removeFromCategory = function( category, title ) {
		var op = 'Удаление статьи «' + title + '» из служебного списка категории «' + category + '»…';
		var summaryText = 'Удаление статьи «[[' + title + ']]» из списка категории «' + category + '»';
		return pageGetEditSave(PAGE_PREFIX_LIST + category, 'all', op, 
			removeFromCategoryImpl, {title: title, category: category}, summaryText, 'error');
	};
	
	function removeFromCategoryImpl(content, params) {
		var title = params.title;
		var category = params.category;
		var oldContent = content + '\n';
		if (oldContent.indexOf( '\n[[' + title + ']]\n' ) === -1) {
			mw.notify('Удаление статьи «' + title + '» из служебного списка категории «' + category + '»…'
			+ T_ERR + ' Статья не найдена в служебном списке категории', notifyOptError);
			return undefined;
		}
		var newListContent = oldContent.replace('\n[[' + title + ']]\n', '\n');
		newListContent = newListContent.substring(0, newListContent.length - 1);
		return newListContent;
	}

	this.purge = function() {
		mw.notify( 'Перезагрузка страницы…', notifyOptOk);
		new mw.Api().post( { action: 'purge', titles: getCurrentPage(true) } ).then(function () {
			window.location.reload();
		}, function () {
			mw.notify( 'Не удалось перезагрузить страницу напрямую. Открываем страницу с подтверждением…', notifyOptOk);
			window.location.assign( mw.config.get( 'wgServer' ) + mw.config.get( 'wgScriptPath' ) + '/index.php?action=purge&title='
				+ encodeURIComponent( getCurrentPage(true) ) );
		});
		return;
	};

	function updateProjectTemplatesImpl(content, params) {
		var newLevel = params.newLevel;
		var patterns = [ "\\{\\{статья проекта[^\\}]*\\}\\}", "\\{\\{проект[^\\}]*\\}\\}" ];
		var result = content;
		for ( var i = 0; i < patterns.length; i++ ) {
			var patt = new RegExp( patterns[i], "gi" );
			result = result.replace( patt, function( found, offset, s ) {
				var separatorIndex = found.indexOf( '\|' );
				if ( separatorIndex > 0 ) {
					if ( found.indexOf( 'уровень' ) > 0 ) {
						return found.replace( new RegExp( 'уровень\\s*=\\s*[A-Za-z0-9А-Яа-я]*', 'i' ), 'уровень=' + newLevel );
					} else { // ранее не было параметра уровень
						return found.substring( 0, separatorIndex ) + '\|уровень=' + newLevel + found.substring( separatorIndex, found.length );
					}
				} else { // статья ранее не была оценена совсем
					return found.replace( '}}', '\|уровень=' + newLevel + '\|важность=}}' );
				}
			} );
		}
		return result;
	}

	function updateProjectTemplates(articleTitle, newLevel) {
		return pageGetEditSave(articleTitle, 0, 'Обновление шаблонов проектов… ', 
			updateProjectTemplatesImpl, {newLevel: newLevel}, 'Обновление шаблонов проектов', 'ignore');
	};

	function pageGetEditSave(title, section, operation, editFunc, params, summary, no_page, no_update, no_create) {
		var d = $.Deferred();

		var op1 = 'Получение текста страницы ' + title + '…';
		mw.notify(operation + op1, notifyOptOk);
		var qParams = {titles: title};
		if (section === 0) qParams.rvsection = 0;
		ruWikiQualityArticles.apiQueryLatestRevision(qParams)
		.done( function( result ) {
			var content = '';
			var pageInfo = getFirstObjectValue( result.query.pages );
			if ( !pageInfo.revisions || !pageInfo.revisions[0] || !pageInfo.revisions[0]['*'] ) {
				if (no_page == 'error') {
					mw.notify( operation + op1 + T_ERR + ' Страница не существует.', notifyOptError);
					d.reject( 'Невозможно получить текст страницы ' + title);
					return;
				} else if (no_page == 'ignore') {
					mw.notify( operation + ' Страница не существует. Обновление не требуется. ', notifyOptOk);
					d.resolve();
					return;
				} else {
					mw.notify(operation + op1 + T_OK + ' Страница ещё не создана. ', notifyOptOk);
				}
			} else {
				mw.notify(operation + op1 + T_OK, notifyOptOk);
				mw.notify(operation + 'Анализ текста страницы ' + title + '…', notifyOptOk);
				content = pageInfo.revisions[0]['*'];
			}
			var newContent = editFunc(content, params);
			if (content === undefined) {
				d.reject();
				return;
			} else if (content === newContent ) {
				if (no_update == 'error') {
					mw.notify(operation + 'Что-то пошло не так. Обновление страницы ' + title + ' не удалось.', notifyOptError);
					d.reject();
				} else {
					mw.notify(operation + 'Обновление страницы ' + title + ' не требуется.', notifyOptOk);
					d.resolve();
				}
				return;
			}

			var op2 = 'Сохранение изменений страницы ' + title + '…';
			mw.notify(operation + op2, notifyOptOk);
			var summaryText = summary || title;
			var qParams = {action: 'edit', title: title, summary: summaryText + summarySuffix, text: newContent};
			if (section === 0) qParams.section = 0;
			if (no_create == true) qParams.nocreate = true;
			new mw.Api().postWithEditToken(qParams).done( function() {
				mw.notify(operation + op2 + T_OK, notifyOptOk);
				d.resolve();
			} ).fail( function() {
				mw.notify(operation + op2 + T_ERR, notifyOptError);
				d.reject.apply( d, arguments );
			} );
		} ).fail( function() {
			mw.notify( operation + op1 + T_ERR, notifyOptError);
			d.reject.apply( d, arguments );
		} );

		return d.promise();
	}

	this.addNominateToCancellationButtons = function() {
		if ( $( "#qa-message" ).length === 0 )
		// не является ДС
		return;
		
		RuWikiQualityArticles.addToolboxMenuButton(
		'Номинировать на лишение статуса ДС',
		function() {
			var nominateFormDiv = $( '<div id="ruWikiQualityNominateToCancellation" title="Выдвижение статьи на лишение статуса добротной">'
			+ '<table border="0"><tr><td width="50" align="center">'
			+ '<img src="' + COMMONS_UPLOAD + '/e/e5/Crystal_Clear_action_bookmark_Silver_doubt.svg/40px-Crystal_Clear_action_bookmark_Silver_doubt.svg.png" height="40" width="40"></td><td>'
			+ '<p>Перед выдвижением на лишением статуса ознакомьтесь с <a href="https://ru.wikipedia.org/wiki/Википедия:ТДС">требованиями к добротным статьям</a></b>.<br/>'
			+ 'Пожалуйста, не номинируйте <b>более 3 статей в день</b>. Если номинируете статью впервые, укажите это при номинировании и дождитесь итога по первой номинации, прежде чем действовать дальше.</p>'
			+ '</td></tr></table>'
			+ '<form><fieldset>'
			+ '<textarea name="opiniontext" id="ruWikiQualityNominateToCancellationComment" style="height: 150px;" class="text ui-widget-content ui-corner-all"></textarea>'
			+ '</fieldset></form>'
			+ '<p class="validateTips" style="color:gray">Поле комментария обязательно к заполнению,<br/>ваша подпись будет добавлена автоматически.</p>'
			+ '</div>' );
			
			var nominateCommentField = nominateFormDiv.find( '#ruWikiQualityNominateToCancellationComment' );
			
			nominateFormDiv.dialog( {
				autoOpen: true,
				height: 'auto',
				width: 600,
				modal: true,
				buttons: {
					"Номинировать на лишение статуса": function() {
						var bValid = true;
						nominateCommentField.removeClass( "ui-state-error" );
						bValid = bValid && ruWikiQualityArticles.checkNotEmpty( nominateCommentField );
						
						if ( bValid ) {
							$( this ).dialog( "close" );
							ruWikiQualityArticles.nominateToCancellation( nominateCommentField.val() );
						}
					},
					"Отменить": function() {
						$( this ).dialog( "close" );
					}
				}
			} )
		} );
	};
	
	this.nominateToCancellation = function( comment ) {
		var replaceTemplateInArticle = this.nominateToCancellation_replaceTemplate( mw.config.get( 'wgTitle' ), '{{Добротная статья|', '{{К лишению статуса добротной|'
		+ RuWikiQualityArticles.getCurrentDateWikitext() + '|' );
		var addToDiscussionPage = this.nominateToCancellation_addToDiscussionPage( mw.config.get( 'wgTitle' ), comment );
		
		$.when( replaceTemplateInArticle, addToDiscussionPage ).done( function() {
			ruWikiQualityArticles.purge();
		} );
	};
	
	this.nominateToCancellation_addToDiscussionPage = function( articleTitle, comment ) {
		var operation = 'Номинирование на лишение статуса… Создание секции обсуждения… ';
		mw.notify( operation, notifyOptOk);
		return new mw.Api().postWithEditToken( {
			action: 'edit',
			title: PAGE_CANCEL_DISC,
			section: 0,
			appendtext: '\n\n== [[' + articleTitle + ']] ==\n' + comment + T_SIGN_A + '\n',
			summary: 'Номинирование «[[' + articleTitle + ']]» на лишение статуса добротной' + RuWikiQualityArticles.summarySuffix,
			} ).done( function() {
			mw.notify( operation + T_OK, notifyOptOk);
			} ).fail( function() {
			mw.notify( operation + 'Неизвестная ошибка!', notifyOptError);
		} );
	};
	
	this.nominateToCancellation_replaceTemplate = function( articleTitle, oldTemplateName, newTemplateName ) {
		var d = $.Deferred();
		var operation = 'Номинирование на лишение статуса… Замена шаблона добротной статьи… ';
		var operation_p1 = 'Получение текста статьи… ';
		var operation_p2 = 'Сохранение текста статьи… ';
		mw.notify( operation + operation_p1, notifyOptOk);
		new mw.Api().get( {
			action: 'query',
			prop: 'revisions',
			rvprop: 'content',
			titles: articleTitle,
			} ).done( function( result ) {
			var pageInfo = RuWikiQualityArticles.getFirstObjectValue( result.query.pages );
			var content = pageInfo.revisions[0]['*'];
			var newContent = content.replace( new RegExp( "(" + $.ui.autocomplete.escapeRegex( oldTemplateName ) + ")", "gi" ), newTemplateName );
			mw.notify( operation + operation_p2, notifyOptOk);
			
			new mw.Api().postWithEditToken( {
				action: 'edit',
				title: articleTitle,
				summary: 'Номинирование на лишение статуса добротной' + RuWikiQualityArticles.summarySuffix,
				text: newContent,
				} ).done( function() {
				mw.notify( operation + operation_p2 + T_OK, notifyOptOk);
				d.resolve();
				} ).fail( function() {
				mw.notify( operation + operation_p2 + ' Неизвестная ошибка!', notifyOptError);
				d.reject.apply( d, arguments );
			} );

			} ).fail( function() {
			mw.notify( operation + operation_p1 + T_ERR, notifyOptError);
			console.log( arguments );
			d.reject.apply( d, arguments );
		} );
		
		return d.promise();
	};
};

RuWikiQualityArticles.summarySuffix = ' с помощью гаджета QA (v. ' + mw.loader.moduleRegistry['ext.gadget.qualityArticles'].version + ')';

RuWikiQualityArticles.addToolboxMenuButton = function( label, click ) {
	$( "#p-tb div ul" ).append( $( '<li class="plainlinks"></li>' ).append( $( document.createElement( 'a' ) ).text( label ).css( 'cursor', 'pointer' ).click( click ) ) );
};

RuWikiQualityArticles.getFirstObjectValue = function( obj ) {
	"use strict";
	return obj[Object.keys( obj )[0]];
};

RuWikiQualityArticles.getCurrentDateWikitext = function() {
	"use strict";
	return '{{su' + 'bst:CURRENTDAY}} {{su' + 'bst:CURRENTMONTHNAMEGEN}} {{su' + 'bst:CURRENTYEAR}}';
};

mw.loader.using( [ 'jquery.ui', 'mediawiki.api' ], function() {
	var ruWikiQualityArticles = new RuWikiQualityArticles();
	ruWikiQualityArticles.addButtonsDiscussion();
	ruWikiQualityArticles.addButtonsArchive();
	ruWikiQualityArticles.addButtonsNominate();
	ruWikiQualityArticles.addButtonsChangeCategories();
	ruWikiQualityArticles.addNominateToCancellationButtons();
	ruWikiQualityArticles.addFinalDialog();
}, function () {
	console.log('Dependency Error! DS may not work!');
	mw.notify( 'Ошибка при загрузке зависимостей. Гаджет может не работать, или работать с ошибками!',
		{ autoHide: false, type: 'error', tag: 'QA-Gadget' } );
} );

// </nowiki>