MediaWiki:Gadget-GlobalUsage.js
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.
This user script seems to have a documentation page at MediaWiki:Gadget-GlobalUsage. |
/**
* 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 ));