MediaWiki:Gadget-Merge.js
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/*!
* merge.js - Script to merge Wikidata items
*
* Written by Ebraminio
* modified by Ricordisamoa
*
* Released under CC-Zero
*
*/
/*jslint browser: true, regexp: true, indent: 2, unparam: true*/
/*global jQuery: false, mediaWiki: false, console: false*/
//<nowiki>
(function ($, mw) {
'use strict';
var formName = 'merge-form';
/**
* Display progress on form dialog
*/
function displayProgress(message) {
$('#' + formName + ' div').hide();
$('<div />')
.css({
'text-align': 'center',
'margin': '3em 0',
'font-size': '120%'
})
.text(message)
.append('<br/><br/>')
.append(
$.createSpinner({
size: 'large',
type: 'block'
})
).appendTo('#' + formName);
}
/**
* Display error on form dialog
*/
function displayError(error) {
$('#' + formName + ' div').hide();
$('#' + formName).append($('<div />', {
'style': 'color: #990000; margin-top: 0.4em;',
'html': 'Error: ' + error
}));
}
/**
* Check if the user is an admin, and thus can delete items
*/
function canDelete() {
return mw.config.get('wgUserGroups').indexOf("sysop") !== -1;
}
/**
* Retrieve items by id
*/
function getItems(ids, callback) {
new mw.Api().get({
action: 'wbgetentities',
ids: ids.join('|'),
format: 'json'
}).done(function (data) {
callback($.map(data.entities, function (x) { return x; }));
});
}
/**
* Check if items can be merged
*/
function detectConflicts(items) {
var all = {},
conflicts = {};
$.each(items, function (i) {
if (items[i].sitelinks !== undefined) {
$.each(items[i].sitelinks, function (j) {
if (all[j] !== undefined) {
if (conflicts[j] === undefined) {
conflicts[j] = [all[j]];
}
conflicts[j].push(items[i]);
}
all[j] = items[i];
});
}
});
return conflicts;
}
function mergePending(id) {
$.jStorage.set('merge-pending-id', id);
mw.notify($.parseHTML('Merge.js has been started.<br />Now you can focus your browser on the other item and refresh the page.'));
}
/**
* Sort items by their Q## id. Useful for detecting eligible item to merged into.
*/
function sortItemsId(items) {
return $.map($.map(items, function (item) {
return {
item: item,
id: parseInt(item.id.replace(/^Q/i, ''), 10)
};
}).sort(function (x, y) { return x.id - y.id; }), function (item) {
return item.item;
});
}
/**
* Logger function
*/
function log(m) {
console.log(m);
}
/**
* Item editor
*/
function editItem(id, data, summary, callback) {
if (id === undefined) {
displayError("the item's id is not defined");
return;
}
data.id = undefined;
new mw.Api().post({
action: "wbeditentity",
id: id,
data: JSON.stringify(data),
token: mw.user.tokens.get('editToken'),
summary: summary
}).always(log).done(callback);
}
/**
* Item deleter
*/
function deleteItem(id, reason, callback) {
if (id === undefined) {
displayError("the item's id is not defined");
return;
}
if (canDelete() === false) {
displayError("you're not an administrator");
return;
}
displayProgress('Deleting the item...');
new mw.Api().post({
action: "delete",
title: id,
reason: reason,
token: mw.user.tokens.get('editToken')
}).always(log).done(callback);
}
/**
* Clone just needed things
*/
function cloneCleanItem(item) {
return {
id: item.id,
sitelinks: $.extend({}, item.sitelinks),
labels: $.extend({}, item.labels),
descriptions: $.extend({}, item.descriptions)
};
}
/**
* Check equality of claims
*/
function isEqualClaim(claimFrom, claimTo) {
if (claimFrom.mainsnak === undefined && claimTo.mainsnak === undefined) {
return true; // nothing is available to merge
}
if (claimFrom.mainsnak.datavalue === undefined || claimTo.mainsnak.datavalue === undefined) {
throw new Error("Oh, this is not cool");
}
// this must be refactored
return JSON.stringify(claimFrom.mainsnak.datavalue) === JSON.stringify(claimTo.mainsnak.datavalue);
}
/**
* Add a claim
*/
function addClaim(to, claim, callback) {
new mw.Api().post({
action: 'wbcreateclaim',
entity: to.id,
property: claim.mainsnak.property,
snaktype: claim.mainsnak.snaktype,
value: JSON.stringify(claim.mainsnak.datavalue.value),
token: mw.user.tokens.get('editToken')
}).always(log).done(callback);
}
/**
* Abstraction to support multiple ajax calls
*/
function AsyncCountDown(count, callback) {
if (count === 0) {
callback();
return;
}
this.count = count;
this.step = function () {
this.count = this.count - 1;
if (this.count === 0) {
this.end();
}
};
this.end = callback;
}
/**
* Claim moving logic
*/
function moveClaims(from, to, callback) {
var claimsToBeAdd = [],
claims = from.claims,
counter;
if (claims !== undefined) {
$.each(claims, function (name) {
$.each(from.claims[name], function (i) {
var claim = from.claims[name][i],
available = false;
if (to.claims !== undefined && to.claims[name] !== undefined) {
$.each(to.claims[name], function (j) {
if (claim.mainsnak === undefined || isEqualClaim(claim, to.claims[name][j])) {
available = true;
}
});
}
if (available) {
return;
}
claimsToBeAdd.push(claim);
});
});
}
console.log(claimsToBeAdd);
counter = new AsyncCountDown(claimsToBeAdd.length, callback); // Counter will fire our callback
$.each(claimsToBeAdd, function (i) {
addClaim(to, claimsToBeAdd[i], function () {
counter.step();
});
});
}
/**
* Moving logic
*/
function moveItemContent(from, to, callback) {
var newFrom = cloneCleanItem(from), // clone
newTo = cloneCleanItem(to);
$.each(newFrom.sitelinks, function (x) {
newTo.sitelinks[x] = $.extend({}, newFrom.sitelinks[x]);
newFrom.sitelinks[x].title = '';
});
$.each(newFrom.labels, function(x) {
newTo.labels[x] = $.extend({}, newFrom.labels[x]);
newFrom.labels[x].value = '';
});
$.each(newFrom.descriptions, function(x) {
newTo.descriptions[x] = $.extend({}, newFrom.descriptions[x]);
newFrom.descriptions[x].value = '';
});
// It must be done consequently
editItem(newFrom.id, newFrom, 'Removed by merge.js, moving to [[' + newTo.id + ']]', function () {
editItem(newTo.id, newTo, 'Added by merge.js, moving from [[' + newFrom.id + ']]', function () {
moveClaims(from, to, callback); // Original from and to objects, not new cleaned ones
});
});
}
// Copy edited [[MediaWiki:Gadget-RequestDeletion.js]]
function requestDeletion(entity, success, reason) {
displayProgress('Requesting deletion for item');
new mw.Api().post({
'format': 'json',
'action': 'edit',
'title': 'Wikidata:Requests for deletions',
'summary': '/* ' + entity + ' */ requested deletion ([[User:Ricordisamoa/merge.js|merge.js]])',
'appendtext': '\n\n{{subst:Request for deletion|itemid=' + entity + '|reason=' + reason + '}}' + ' ~~' + '~~',
'token': mw.user.tokens.get('editToken')
}).done(function (data) {
if (data.error && data.error.info) {
displayError(data.error.info);
} else {
success();
}
}).fail(function (data) {
displayError(data);
}).always(log);
}
function requestStreamDeletion(entity, success, mergedTo) {
new mw.Api().post({
'action': 'edit',
'appendtext': '\n{{/row|' + entity.replace(/^Q/i, '') + '|to=' + mergedTo.replace(/^Q/i, '') + '}}',
'title': 'User:Ricordisamoa/StreamDelete',
'summary': '[[' + entity + ']]: requested StreamDeletion ([[User:Ricordisamoa/merge.js|merge.js]])',
'token': mw.user.tokens.get('editToken')
}).done(function (data) {
if (data.error && data.error.info) {
displayError(data.error.info);
} else {
success();
}
}).fail(function (data) {
displayError(data);
}).always(log);
}
/**
* Move a batch of items
*/
function batchMover(items) {
displayProgress('Please wait...');
$.each(items.from, function (i) {
var x = items.from[i],
sitelinks = x.sitelinks,
count = 0,
counter;
if (sitelinks === undefined) {
sitelinks = [];
}
if (items.from !== undefined) {
count = items.from.length;
}
counter = new AsyncCountDown(count, function () {
window.location = mw.util.wikiGetlink(items.to.id);
});
moveItemContent(x, items.to, function () {
$.jStorage.deleteKey('merge-pending-id'); // reset the Storage
if (canDelete() && $('#merge-delete')[0].checked === true) {
deleteItem(
x.id.toUpperCase(),
'Merged with [[' + items.to.id.toUpperCase() + ']] ([[User:Ebraminio/merge.js|merge.js]])',
function () {
counter.step();
}
);
} else if ($('#merge-send-to-rfd')[0].checked === true) {
requestDeletion(x.id.toUpperCase(), function () {
counter.step();
}, 'Merged with [[' + items.to.id.toUpperCase() + ']]');
} else if ($('#merge-streamdelete')[0].checked === true) {
requestStreamDeletion(x.id, function () {
counter.step();
}, items.to.id);
} else {
counter.step();
}
});
});
}
/**
* Merge button
*/
function merge(itemsName) {
getItems(itemsName, function (items) {
var conflicts = detectConflicts(items),
message;
if ($.map(conflicts, function (x) { return x; }).length === 0) {
items = sortItemsId(items); // try to sort by Qid
batchMover({
from: items.slice(1),
to: items[0]
});
} else {
message = $.map(conflicts, function (x, i) {
return '<br />A conflict detected on ' + i.replace(/wiki$/, '') + ':' + $.map(x, function (y, j) {
return ' [[' + x[j].id.toUpperCase() + ']] with [[' + i.replace(/wiki$/, '') + ':' + y.sitelinks[i].title + ']]';
}).join(',');
}).join('').replace(/\[\[(.*?)\]\]/g, function (x, y) {
return '<a href="' + mw.util.wikiGetlink(y) + '">' + y + '</a>';
});
displayError(message);
}
});
}
/**
* Dialog creator and launcher
*/
function launchDialog(id) {
if (id !== undefined) {
id = '';
} else {
id = id.toUpperCase();
}
$('<div />', {
'id': formName,
'style': 'position: relative;'
}).append(
$('<div />', {
'style': 'margin-top: 0.4em;',
'html': 'Merge into: '
}).append(
$('<input />', {
'id': 'merge-items',
'style': 'padding: 1px; vertical-align: baseline;',
'value': id
})/*.entityselector({
url: mw.util.wikiScript('api')
}).on('entityselectorselect', function(e, ui) {
$(this).attr("data-selected-id", ui.item.id);
})*/
).append(
$('<div />').append(
$('<input />', {
'id': 'merge-send-to-rfd',
'name': 'merge-send-to-rfd',
'type': 'checkbox'
})
).append(
$('<label />', {
'id': 'merge-send-to-rfd-label',
'for': 'merge-send-to-rfd',
'text': 'Request deletion for this item on RfD'
})
)
).append(
$('<div />').append(
$('<input />', {
'id': 'merge-streamdelete',
'name': 'merge-streamdelete',
'type': 'checkbox'
})
).append(
$('<label />', {
'id': 'merge-streamdelete-label',
'for': 'merge-streamdelete',
'html': 'Request <a href="' + mw.util.wikiGetlink('User:Ricordisamoa/StreamDelete') + '">StreamDeletion</a> for this item (experimental)'
})
)
).append(!canDelete() ? '' : $('<div />').append(
$('<input />', {
'id': 'merge-delete',
'name': 'merge-delete',
'type': 'checkbox'
}).append(
$('<label />', {
'id': 'merge-delete-label',
'for': 'merge-delete',
'text': 'Try to automatically delete this item after merge (only admins)'
})
)
))
).dialog({
width: 500,
autoOpen: false,
title: 'Merge Wizard',
modal: true,
buttons: [{
text: 'Close',
specialButton: 'cancel',
click: function () {
$(formName).remove();
}
}, {
text: 'Merge',
specialButton: 'proceed',
click: function () {
// $('#merge-items').attr("data-selected-id") must be used if entityselector is enabled
merge([$('#merge-items').val(), mw.config.get('wbEntityId')]);
}
}]
}).dialog('open');
}
// Initialization
if (mw.config.get('wgNamespaceNumber') === 0 && mw.config.get('wgAction') === 'view') {
mw.loader.using(['jquery.ui.dialog', 'jquery.jStorage', 'jquery.spinner'], function() {
$(function() {
if ($.jStorage.get('merge-pending-id') !== null && $.jStorage.get('merge-pending-id') !== '' && $.jStorage.get('merge-pending-id').toLowerCase() !== mw.config.get('wbEntityId').toLowerCase()) {
$('#merge-queue-process').remove();
$('<img>')
.attr('src', '//upload.wikimedia.org/wikipedia/commons/thumb/1/10/Pictogram_voting_merge.svg/28px-Pictogram_voting_merge.svg.png')
.wrap('<a>').parent()
.attr({
'href': '#',
'title': 'process the merge'
})
.click(function (event) {
event.preventDefault();
launchDialog($.jStorage.get('merge-pending-id'));
})
.wrap('<li>').parent()
.attr('id', 'merge-queue-process')
.prependTo('#p-views ul');
}
$('#ca-merge').remove();
$(mw.util.addPortletLink('p-cactions', '#', 'Merge it with...', 'ca-merge', 'Merge this item into another and send this to RfD'))
.click(function (event) {
event.preventDefault();
$('#merge-queue-process').remove();
mergePending(mw.config.get('wbEntityId'));
// launchDialog();
});
});
});
}
}(jQuery, mediaWiki));