MediaWiki:Gadget-GlobalUsage.js

From Wikimedia Commons, the free media repository
Jump to navigation Jump to search
Note: After saving, you have to bypass your browser's cache to see the changes. Internet Explorer: press Ctrl-F5, Mozilla: hold down Shift while clicking Reload (or press Ctrl-Shift-R), Opera/Konqueror: press F5, Safari: hold down Shift + Alt while clicking Reload, Chrome: hold down Shift while clicking Reload.
/**
 * GlobalUsage query for files en masse
 * Required because you can't limit global usage on 
 * a per-file-basis
 * https://bugzilla.wikimedia.org/show_bug.cgi?id=36912
 *
 * @rev 2017-12-06
 * @author Rillke, 2012
 * @license This software is quadruple-licensed under GFDL, LGPL, GPL and CC-By-SA 3.0
 * Choose the license(s) you like best
 *
 * Usage instructions: See "GuqMembers = {"
 */
// List the global variables for jsHint-Validation. Please make sure that it passes http://jshint.com/
// Scheme: globalVariable:allowOverwriting[, globalVariable:allowOverwriting][, globalVariable:allowOverwriting]
/*global jQuery:false, mediaWiki:false*/
 
// Set jsHint-options. You should not set forin or undef to false if your script does not validate.
/*jshint forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:true, strict:true, undef:true, curly:false, browser:true*/

 
( function ( $, mw ) {
'use strict';
 
var GuqMembers, guqPrivate;

function Guq(usageThreshold, batchSize, threads, showTipsy, params) {
	// Invoked as a constructor
	if (this.usageThreshold) {
		if (usageThreshold) this.usageThreshold = usageThreshold;
		if (batchSize) this.batchSize = batchSize;
		if (threads) this.threads = threads;
		if (params) this.params = params;
		if (undefined !== showTipsy) this.showTipsy = showTipsy;
		this.$progress = new $.Deferred();
		return this;
	// Invoked as a function
	} else {
		return new Guq(usageThreshold, batchSize, threads, showTipsy, params);
	}
}
guqPrivate = {
	getObjLen: function(obj) {
		var i = 0;
		for (var elem in obj) {
			if (obj.hasOwnProperty(elem)) i++;
		}
		return i;
	},
	each: function(obj, cb) {
		var i = 0;
		for (var elem in obj) {
			if (obj.hasOwnProperty(elem)) {
				if (false === cb(i, elem, obj[elem])) break;
				i++;
			}
		}
		return obj;
	}
};
GuqMembers = {
	constructor: Guq,
	// How many usages to show / retrieve per file
	usageThreshold: 10,
	// How many files to query at once
	batchSize: 10,
	// How many queries to run simultaneously
	threads: 1,
	// Show a tipsy-tooltip over the red badge. Tipsy must be loaded before!
	showTipsy: true,
	// Tipsy gravity (autoNS by default)
	tipsyGravity: 0,
	// Parameters: Object of filenames as keys and $jQuery-DOM-Nodes as values
	// $jQuery-DOM-Nodes are optional, you can pass 0 or false as well
	// If you pass $jQuery-DOM-Nodes, jquery.badge must be loaded before!
	params: null,
	/**
	* Start the query for global usage.
	*
	* @example
	*      window.mw.libs.GlobalUsage().query({ 'FileName': '$jQuery-DOM-Node' }).progress(__myProgressCallback).done(function() { console.log("GlobalUsage> DONE!") });
	*
	* @param params {Object} .
	* @context {window.mw.libs.GlobalUsage} Object with filenames as keys and 
	*    $jQuery-DOM-Nodes (for UI-integration) or 0 (for pure library functionality) as values
	* @return {Object} jQuery Deferred object (http://api.jquery.com/category/deferred-object/)
	*/
	query: function(params) {
		var guq = this,
			all = 0,
			done = 0,
			open = 0,
			usageThreshold = Math.min(this.usageThreshold || 1, 499),
			batchSize = Math.min(this.batchSize || 10, 30), // 30 is an API limit
			threads = this.threads || 1,
			showTipsy = this.showTipsy,
			tipsyGravity = this.tipsyGravity,
			freeThreads = threads,
			allFiles = [],
			_startThread,
			_loadBalance,
			_checkReady,
			_notify,
			_tipsySettings,
			_getTipsyContent,
			_tipsify,
			$progress = this.$progress,
			globalUsage = {};
			
		if (!params) params = guq.params;
		if (!params) return $progress.reject("Error: No files to work on supplied to GlobalUsage.");
		
		open = all = guqPrivate.getObjLen(params);
		
		_startThread = function(filesToWorkOn) {
			if (!filesToWorkOn || !filesToWorkOn.length) return;
			
			var toQuery = filesToWorkOn.slice(0, batchSize),
				remaining = filesToWorkOn.slice(batchSize),
				limit = Math.min(500, Math.max(usageThreshold * toQuery.length + 20,  toQuery.length * 16 + 20)),
				mwa = new mw.Api(),
				apiDef = mwa.post({
					prop: 'globalusage',
					titles: toQuery.join('|'),
					gulimit: limit
				});
				
			freeThreads--;
			
			apiDef.done(function(result) {
				freeThreads++;
				if (!result.query || !result.query.pages) return $progress.reject("API response empty: " + toQuery);
				var partialUsage = {},
					partialUsagePositions = {};
				
				guqPrivate.each(result.query.pages, function(i, id, pg) {
					if (!pg.globalusage) return;
					partialUsage[pg.title] = pg.globalusage.length;
					partialUsagePositions[pg.title] = pg.globalusage;
					limit -= pg.globalusage.length;
					if (!limit) {
						if (pg.globalusage.length < usageThreshold) {
							remaining.push(pg.title);
						} else {
							// TODO: Make use of the continue-param (query-continue.globalusage.gucontinue  --> gucontinue)
							// if less than usageThreshold for one file was returned
							// Indicate there could be more usage than told to have:
							partialUsage[pg.title] = pg.globalusage.length + '+';
						}
					}
				});
				$.extend(globalUsage, partialUsage);
				_notify(globalUsage, partialUsage, partialUsagePositions);
				_loadBalance(remaining);
			});
			apiDef.fail(function(code, result) {
				freeThreads++;
				$progress.reject("API failure: " + code);
			});
		};
		_loadBalance = function(toQuery) {
			var fileCount = toQuery.length,
				filesPerThread = Math.floor(fileCount / freeThreads) + 1,
				filesForThread = [];
				
			_checkReady(toQuery);
			if (!fileCount) return;
			for (var i = 0; i < fileCount; i++) {
				var name = toQuery[i];
				if (i % filesPerThread) {
					filesForThread.push(name);
				} else {
					_startThread(filesForThread);
					filesForThread = [name];
				}
			}
			_startThread(filesForThread);
		};
		_checkReady = function(remainingFiles) {
			if (!remainingFiles.length && freeThreads === threads) {
				$progress.resolve("All global usage retrieved.", globalUsage);
			}
		};
		_notify = function(gu, pu, pup) {
			$progress.notify("New globalUsage available", pu, gu, pup);
			// Update the UI
			guqPrivate.each(pu, function(i, name, count) {
				var $el = params[name];
				if ($el) {
					$el.badge(count, 'bottom', true);
					$el.data('globalUsage', pup[name]);
					$el.data('globalUsageFN', name);
					_tipsify($el);
				}
			});
		};
		_tipsify = function($el) {
			if (showTipsy) $el.children('.notification-badge').tipsy(_tipsySettings);
		};
		if (showTipsy) {
			_tipsySettings = {
				title: function() {
					return guq._getTipsyContent.apply(this, [usageThreshold]);
				},
				delayOut: 1000,
				html: true,
				gravity: tipsyGravity || $.fn.tipsy.autoNS
			};
		}
		
		// Initial thread balancer
		guqPrivate.each(params, function(i, name, $el) {
			allFiles.push(name);
		});
		_loadBalance(allFiles);
		
		return $progress;
	},
	_getTipsyContent: function(usageThreshold) {
		$('.tipsy').remove();
		var $parent = $(this).parent(),
			gus = $parent.data('globalUsage'),
			name = $parent.data('globalUsageFN'),
			lastWiki = '',
			r =  "<b><i>Global usage:</i></b><br/>",
			exceeded = false,
			title;
			
		guqPrivate.each(gus, function(i, i2, gu) {
			if (i >= usageThreshold) {
				exceeded = true;
				return false;
			}
			try {
				title = mw.html.escape(gu.title);
			} catch(ex) {
				title = '';
			}
			if (lastWiki !== gu.wiki) {
				if (lastWiki) r += '</ul>';
				lastWiki = gu.wiki;
				r += '<b>' + mw.html.escape(gu.wiki) + '</b><ul style="font-size:smaller">';
			}
			r += '<li><a href="' + gu.url + '" target="_blank">' + title.replace(/_/g, ' ') + '</a></li>';
		});
		if (gus.length) { 
			r += '</ul>'; 
			if (exceeded) r+= '<a style="text-align:right; font-size:smaller; display:block;"  target="_blank" href="' + 
				mw.util.getUrl('Special:GlobalUsage/' + name.replace(/^File:/, '')) + '">More…</a>';
		}
		return r;
	}
};
// Add members to prototype
Guq.fn = Guq.prototype = GuqMembers;
 
// Expose globally
window.mw.libs.GlobalUsage = Guq;

}( jQuery, mediaWiki ));