MediaWiki:Gadget-Merge.js: Difference between revisions

From Wikidata
Jump to navigation Jump to search
Content deleted Content added
No edit summary
No edit summary
Line 8: Line 8:
*
*
*/
*/
/*jslint browser: true, regexp: true, indent: 2, unparam: true*/
/*global jQuery: false, mediaWiki: false, console: false*/
//<nowiki>
//<nowiki>
(function ($, mw) {
(function ($, mw, undefined) { // third argument is fake and is just to make sure undefined really is undefined!
'use strict';
'use strict';


var formName = 'merge-form';
var formName = 'merge-form';


/**
/**
* Display progress on form dialog
* Display progress on form dialog
*/
*/
function displayProgress(message) {
function displayProgress(message) {
$('#' + formName + ' div').hide();
$('#' + formName + ' div').hide();
$('<div />')
$('<div />')
.css({
.css({
'text-align': 'center',
'text-align': 'center',
'margin': '3em 0',
'margin': '3em 0',
'font-size': '120%'
'font-size': '120%'
})
})
.text(message)
.text(message)
.append('<br/><br/>')
.append('<br/><br/>')
.append($.createSpinner({
.append(
$.createSpinner({
size: 'large',
size: 'large',
type: 'block'
type: 'block'
}))
})
.appendTo('#' + formName);
).appendTo('#' + formName);
}
}


/**
/**
* Display error on form dialog
* Display error on form dialog
*/
*/
function displayError(error) {
function displayError(error) {
$('#' + formName + ' div').hide();
$('#' + formName + ' div').hide();


$('#' + formName).append($('<div />', {
$('#' + formName).append($('<div />', {
'style': 'color: #990000; margin-top: 0.4em;',
'style': 'color: #990000; margin-top: 0.4em;',
'html': 'Error: ' + error
'html': 'Error: ' + error
}));
}));
}
}


/**
/**
* Check if the user is an admin, and thus can delete items
* Check if the user is an admin, and thus can delete items
*/
*/
function canDelete() {
function canDelete() {
return wgUserGroups.indexOf("sysop") !== -1;
return mw.config.get('wgUserGroups').indexOf("sysop") !== -1;
}
}


/**
/**
* Retrieve items by id
* Retrieve items by id
*/
*/
function getItems(ids, callback) {
function getItems(ids, callback) {
new mw.Api().get({
new mw.Api().get({
action: 'wbgetentities',
action: 'wbgetentities',
ids: ids.join('|'),
ids: ids.join('|'),
format: 'json'
format: 'json'
}).done(function (data) {
}).done(function (data) {
callback($.map(data.entities, function (x) { return x; }));
callback($.map(data.entities, function (x) { return x; }));
});
});
}
}


/**
/**
* Check if items can be merged
* Check if items can be merged
*/
*/
function detectConflicts(items) {
function detectConflicts(items) {
var all = {},
var all = {},
conflicts = {};
conflicts = {};
$.each(items, function (i, item) {
$.each(items, function (i) {
if (item.sitelinks !== undefined) {
if (items[i].sitelinks !== undefined) {
$.each(item.sitelinks, function (i, x) {
$.each(items[i].sitelinks, function (j) {
if (all[i] !== undefined) {
if (all[j] !== undefined) {
if (conflicts[i] === undefined) {
if (conflicts[j] === undefined) {
conflicts[i] = [all[i]];
conflicts[j] = [all[j]];
}
conflicts[j].push(items[i]);
}
}
conflicts[i].push(item);
all[j] = items[i];
}
});
all[i] = item;
}
});
});
return conflicts;
}
});
}
return conflicts;
}


function mergePending(id) {
function mergePending(id) {
$.jStorage.set('merge-pending-id', 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.'));
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 serialized size. Useful for detecting eligible item to merged into.
* Sort items by their Q## id. Useful for detecting eligible item to merged into.
*/
* NOT USED CURRENTLY
function sortItemsId(items) {
*/
return $.map($.map(items, function (item) {
function sortItemsDesc(items) {
return {
return $.map($.map(items, function (item) {
item: item,
return {
item: item,
id: parseInt(item.id.replace(/^Q/i, ''), 10)
};
size: JSON.stringify(item).length
}).sort(function (x, y) { return x.id - y.id; }), function (item) {
};
return item.item;
}).sort(function (x, y) { return y.size - x.size; }), function (item) {
return item.item;
});
});
}
}


/**
/**
* Logger function
* Sort items by their Q## id. Useful for detecting eligible item to merged into.
*/
*/
function sortItemsId(items) {
function log(m) {
console.log(m);
return $.map($.map(items, function (item) {
}
return {
item: item,
id: parseInt(item.id.replace(/^Q/i, ''))
};
}).sort(function (x, y) { return x.id - y.id; }), function (item) {
return item.item;
});
}


/**
/**
* Item editor
* Logger function
*/
*/
function log(m) {
function editItem(id, data, summary, callback) {
if (id === undefined) {
console.log(m);
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 editor
* Item deleter
*/
*/
function editItem(id, data, summary, callback) {
function deleteItem(id, reason, callback) {
if (id === undefined) {
if (id === undefined) {
displayError("the item's id is not defined");
displayError("the item's id is not defined");
return;
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);
}
}
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);
}


/**
/**
* Clone just needed things
* Item deleter
*/
*/
function cloneCleanItem(item) {
function deleteItem(id, reason, callback) {
if (id === undefined) {
return {
id: item.id,
displayError("the item's id is not defined");
sitelinks: $.extend({}, item.sitelinks),
return;
labels: $.extend({}, item.labels),
descriptions: $.extend({}, item.descriptions)
};
}
}
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);
}


/**
/**
* Check equality of claims
* Clone just needed things
*/
*/
function cloneCleanItem(item) {
function isEqualClaim(claimFrom, claimTo) {
if (claimFrom.mainsnak === undefined && claimTo.mainsnak === undefined) {
return {
return true; // nothing is available to merge
id: item.id,
}
sitelinks: $.extend({}, item.sitelinks),
labels: $.extend({}, item.labels),
descriptions: $.extend({}, item.descriptions)
};
}


if (claimFrom.mainsnak.datavalue === undefined || claimTo.mainsnak.datavalue === undefined) {
/**
throw new Error("Oh, this is not cool");
* Check equality of claims
}
*/
// this must be refactored
function isEqualClaim(claimFrom, claimTo) {
if (claimFrom.mainsnak === undefined && claimTo.mainsnak === undefined) {
return JSON.stringify(claimFrom.mainsnak.datavalue) === JSON.stringify(claimTo.mainsnak.datavalue);
return true; // nothing is available to merge
}
}


/**
if (claimFrom.mainsnak.datavalue === undefined || claimTo.mainsnak.datavalue === undefined) {
* Add a claim
throw new Error("Oh, this is not cool");
*/
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);
}
}
// this must be refactored
return JSON.stringify(claimFrom.mainsnak.datavalue) === JSON.stringify(claimTo.mainsnak.datavalue);
}


/**
/**
* Abstraction to support multiple ajax calls
* Add a claim
*/
*/
function addClaim(to, claim, callback) {
function AsyncCountDown(count, callback) {
if (count === 0) {
new mw.Api().post({
callback();
action: 'wbcreateclaim',
entity: to.id,
return;
}
property: claim.mainsnak.property,
this.count = count;
snaktype: claim.mainsnak.snaktype,
this.step = function () {
value: JSON.stringify(claim.mainsnak.datavalue.value),
this.count = this.count - 1;
token: mw.user.tokens.get('editToken')
if (this.count === 0) {
}).always(log).done(callback);
this.end();
}
}
};
this.end = callback;
}


/**
/**
* Claim moving logic
* Claim moving logic
*/
*/
function moveClaims(from, to, callback) {
function moveClaims(from, to, callback) {
var claimsToBeAdd = [],
var claimsToBeAdd = [],
claims = from.claims;
claims = from.claims,
if (claims !== undefined) {
counter;
if (claims !== undefined) {
$.each(claims, function (name) {
$.each(claims, function (name) {
$.each(from.claims[name], function (i, claim) {
$.each(from.claims[name], function (i) {
var available = false;
var claim = from.claims[name][i],
if (to.claims !== undefined && to.claims[name] !== undefined) {
available = false;
if (to.claims !== undefined && to.claims[name] !== undefined) {
$.each(to.claims[name], function (j) {
$.each(to.claims[name], function (j) {
if (claim.mainsnak === undefined || isEqualClaim(claim, to.claims[name][j])) {
if (claim.mainsnak === undefined || isEqualClaim(claim, to.claims[name][j])) {
available = true;
available = true;
}
}
});
});
}
}
if (available) {
if (available) {
return;
return;
}
}
claimsToBeAdd.push(claim);
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();
});
});
});
});
}
}
console.log(claimsToBeAdd);
var counter = new AsyncCountDown(claimsToBeAdd.length, callback); // Counter will fire our callback
$.each(claimsToBeAdd, function (i) {
addClaim(to, claimsToBeAdd[i], function () {
counter.step();
});
});
}


/**
/**
* Moving logic
* Abstraction to support multiple ajax calls
*/
*/
function AsyncCountDown(count, callback) {
function moveItemContent(from, to, callback) {
var newFrom = cloneCleanItem(from), // clone
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;
}

/**
* Moving logic
*/
function moveItemContent(from, to, callback) {
var newFrom = cloneCleanItem(from), // clone
newTo = cloneCleanItem(to);
newTo = cloneCleanItem(to);


$.each(newFrom.sitelinks, function (x) {
$.each(newFrom.sitelinks, function (x) {
newTo.sitelinks[x] = $.extend({}, newFrom.sitelinks[x]);
newTo.sitelinks[x] = $.extend({}, newFrom.sitelinks[x]);
newFrom.sitelinks[x].title = '';
newFrom.sitelinks[x].title = '';
});
});


$.each(newFrom.labels, function(x) {
$.each(newFrom.labels, function(x) {
newTo.labels[x] = $.extend({}, newFrom.labels[x]);
newTo.labels[x] = $.extend({}, newFrom.labels[x]);
newFrom.labels[x].value = '';
newFrom.labels[x].value = '';
});
});


$.each(newFrom.descriptions, function(x) {
$.each(newFrom.descriptions, function(x) {
newTo.descriptions[x] = $.extend({}, newFrom.descriptions[x]);
newTo.descriptions[x] = $.extend({}, newFrom.descriptions[x]);
newFrom.descriptions[x].value = '';
newFrom.descriptions[x].value = '';
});
});


// It must be done consequently
// It must be done consequently
editItem(newFrom.id, newFrom, 'Removed by merge.js, moving to [[' + newTo.id + ']]', function () {
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 () {
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
moveClaims(from, to, callback); // Original from and to objects, not new cleaned ones
});
});
});
});
}
}


// Copy edited [[MediaWiki:Gadget-RequestDeletion.js]]
// Copy edited [[MediaWiki:Gadget-RequestDeletion.js]]
function requestDeletion(entity, success, reason) {
function requestDeletion(entity, success, reason) {
displayProgress('Requesting deletion for item');
displayProgress('Requesting deletion for item');
new mw.Api().post({
new mw.Api().post({
'format': 'json',
'format': 'json',
'action': 'edit',
'action': 'edit',
'title': 'Wikidata:Requests for deletions',
'title': 'Wikidata:Requests for deletions',
'summary': '/* ' + entity + ' */ requested deletion ([[User:Ricordisamoa/merge.js|merge.js]])',
'summary': '/* ' + entity + ' */ requested deletion ([[User:Ricordisamoa/merge.js|merge.js]])',
'appendtext': '\n\n{{subst:Request for deletion|itemid=' + entity + '|reason=' + reason + '}}' + ' ~~' + '~~',
'appendtext': '\n\n{{subst:Request for deletion|itemid=' + entity + '|reason=' + reason + '}}' + ' ~~' + '~~',
'token': mw.user.tokens.get('editToken')
'token': mw.user.tokens.get('editToken')
}).done(function (data) {
}).done(function (data) {
if (data.error && data.error.info) {
if (data.error && data.error.info) {
displayError(data.error.info);
displayError(data.error.info);
} else {
} else {
success();
success();
}
}
}).fail(function (data) {
}).fail(function (data) {
displayError(data);
displayError(data);
}).always(log);
}).always(log);
}
}


function requestStreamDeletion(entity, success, mergedTo) {
function requestStreamDeletion(entity, success, mergedTo) {
new mw.Api().post({
new mw.Api().post({
'action': 'edit',
'action': 'edit',
'appendtext': '\n{{/row|' + entity.replace(/^Q/i,'') + '|to=' + mergedTo.replace(/^Q/i,'') + '}}',
'appendtext': '\n{{/row|' + entity.replace(/^Q/i, '') + '|to=' + mergedTo.replace(/^Q/i, '') + '}}',
'title': 'User:Ricordisamoa/StreamDelete',
'title': 'User:Ricordisamoa/StreamDelete',
'summary': '[[' + entity + ']]: requested StreamDeletion ([[User:Ricordisamoa/merge.js|merge.js]])',
'summary': '[[' + entity + ']]: requested StreamDeletion ([[User:Ricordisamoa/merge.js|merge.js]])',
'token': mw.user.tokens.get('editToken')
'token': mw.user.tokens.get('editToken')
}).done(function (data) {
})
.done(function (data) {
if (data.error && data.error.info) {
if (data.error && data.error.info) {
displayError(data.error.info);
} else {
displayError(data.error.info);
} else {
success();
success();
}
}).fail(function (data) {
}
displayError(data);
})
}).always(log);
.fail(function (data) {
}
displayError(data);
}).always(log);
}


/**
/**
* Move a batch of items
* Move a batch of items
*/
*/
function batchMover(items, callback) {
function batchMover(items) {
displayProgress('Please wait...');
displayProgress('Please wait...');
$.each(items.from, function (i, x) {
$.each(items.from, function (i) {
var sitelinks = x.sitelinks,
var x = items.from[i],
count = 0;
sitelinks = x.sitelinks,
count = 0,
if (sitelinks === undefined) {
sitelinks = [];
counter;
if (sitelinks === undefined) {
}
sitelinks = [];
if (items.from !== undefined) {
}
count = items.from.length;
if (items.from !== undefined) {
}
var counter = new AsyncCountDown(items.from.length, function () {
count = items.from.length;
}
window.location = mw.util.wikiGetlink(items.to.id);
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();
}
});
});
});
}
moveItemContent(x, items.to, function () {

$.jStorage.deleteKey('merge-pending-id'); // reset the Storage
/**
if (canDelete() && $('#merge-delete')[0].checked === true) {
* Merge button
deleteItem(x.id.toUpperCase(), 'Merged with [[' + items.to.id.toUpperCase() + ']] ([[User:Ebraminio/merge.js|merge.js]])', function () {
*/
counter.step();
function merge(itemsName) {
});
getItems(itemsName, function (items) {
} else if ($('#merge-send-to-rfd')[0].checked === true) {
var conflicts = detectConflicts(items),
requestDeletion(x.id.toUpperCase(), function () {
counter.step();
message;
if ($.map(conflicts, function (x) { return x; }).length === 0) {
}, 'Merged with [[' + items.to.id.toUpperCase() + ']]');
items = sortItemsId(items); // try to sort by Qid
} else if ($('#merge-streamdelete')[0].checked === true) {
batchMover({
requestStreamDeletion(x.id, function () {
counter.step();
from: items.slice(1),
}, items.to.id);
to: items[0]
});
} else {
} else {
counter.step();
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
* Merge button
*/
*/
function merge(itemsName) {
function launchDialog(id) {
if (id !== undefined) {
getItems(itemsName, function (items) {
id = '';
var conflicts = detectConflicts(items),
message;
if ($.map(conflicts, function (x) { return x; }).length === 0) {
var items = sortItemsId(items); // try to sort by Qid
batchMover({
from: items.slice(1),
to: items[0]
});
} else {
} else {
message = $.map(conflicts, function (x, i) {
id = id.toUpperCase();
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 />', {
$('<div />', {
'style': 'margin-top: 0.4em;',
'id': formName,
'html': 'Merge into: '
'style': 'position: relative;'
}).append(
}).append(
$('<input />', {
$('<div />', {
'id': 'merge-items',
'style': 'margin-top: 0.4em;',
'style': 'padding: 1px; vertical-align: baseline;',
'html': 'Merge into: '
'value': id
}).append(
})/*.entityselector({
url: mw.util.wikiScript('api')
}).on('entityselectorselect', function(e, ui) {
$(this).attr("data-selected-id", ui.item.id);
})*/
).append(
$('<div />').append(
$('<input />', {
$('<input />', {
'id': 'merge-send-to-rfd',
'id': 'merge-items',
'name': 'merge-send-to-rfd',
'style': 'padding: 1px; vertical-align: baseline;',
'type': 'checkbox'
'value': id
})
})/*.entityselector({
url: mw.util.wikiScript('api')
}).on('entityselectorselect', function(e, ui) {
$(this).attr("data-selected-id", ui.item.id);
})*/
).append(
).append(
$('<label />', {
$('<div />').append(
'id': 'merge-send-to-rfd-label',
$('<input />', {
'for': 'merge-send-to-rfd',
'id': 'merge-send-to-rfd',
'text': 'Request deletion for this item on RfD'
'name': 'merge-send-to-rfd',
})
'type': 'checkbox'
)
})
).append(
).append(
$('<div />').append(
$('<label />', {
$('<input />', {
'id': 'merge-send-to-rfd-label',
'id': 'merge-streamdelete',
'for': 'merge-send-to-rfd',
'name': 'merge-streamdelete',
'text': 'Request deletion for this item on RfD'
'type': 'checkbox'
})
})
)
).append(
).append(
$('<label />', {
$('<div />').append(
'id': 'merge-streamdelete-label',
$('<input />', {
'for': 'merge-streamdelete',
'id': 'merge-streamdelete',
'name': 'merge-streamdelete',
'html': 'Request <a href="' + mw.util.wikiGetlink('User:Ricordisamoa/StreamDelete') + '">StreamDeletion</a> for this item (experimental)'
})
'type': 'checkbox'
)
})
).append(!canDelete() ? '' :
).append(
$('<div />').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 />', {
$('<input />', {
'id': 'merge-delete',
'id': 'merge-delete',
Line 456: Line 449:
'type': 'checkbox'
'type': 'checkbox'
}).append(
}).append(
$('<label />', {
$('<label />', {
'id': 'merge-delete-label',
'id': 'merge-delete-label',
'for': 'merge-delete',
'for': 'merge-delete',
'text': 'Try to automatically delete this item after merge (only admins)'
'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([$('#merge-items').val()/*.attr("data-selected-id")*/, mw.config.get('wgPageName')]);
}
}]
}).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() !== 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'
})
})
)
))
).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) {
.click(function (event) {
event.preventDefault();
event.preventDefault();
launchDialog($.jStorage.get('merge-pending-id'));
$('#merge-queue-process').remove();
})
mergePending(mw.config.get('wbEntityId'));
.wrap('<li>').parent()
// launchDialog();
.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(wbEntityId);
// launchDialog();
});
});
});
});
}
}


}(jQuery, mediaWiki));
}(jQuery, mediaWiki));

Revision as of 10:21, 23 May 2013

/*!
 * 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));