MediaWiki:Gadget-Merge.js: Difference between revisions
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 |
|||
*/ |
|||
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 |
|||
*/ |
|||
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 |
|||
*/ |
|||
function canDelete() { |
function canDelete() { |
||
return wgUserGroups.indexOf("sysop") !== -1; |
return mw.config.get('wgUserGroups').indexOf("sysop") !== -1; |
||
} |
} |
||
/** |
/** |
||
* 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 |
|||
*/ |
|||
function detectConflicts(items) { |
function detectConflicts(items) { |
||
var all = {}, |
var all = {}, |
||
conflicts = {}; |
conflicts = {}; |
||
$.each(items, function (i |
$.each(items, function (i) { |
||
if ( |
if (items[i].sitelinks !== undefined) { |
||
$.each( |
$.each(items[i].sitelinks, function (j) { |
||
if (all[ |
if (all[j] !== undefined) { |
||
if (conflicts[ |
if (conflicts[j] === undefined) { |
||
conflicts[ |
conflicts[j] = [all[j]]; |
||
} |
|||
conflicts[j].push(items[i]); |
|||
} |
} |
||
all[j] = items[i]; |
|||
} |
}); |
||
} |
|||
}); |
|||
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 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 { |
|||
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) { |
|||
}); |
|||
} |
} |
||
} |
|||
/** |
/** |
||
* Logger function |
|||
* Sort items by their Q## id. Useful for detecting eligible item to merged into. |
|||
*/ |
|||
function |
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 |
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 deleter |
|||
*/ |
|||
function |
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) { |
|||
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 |
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) { |
|||
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 |
function AsyncCountDown(count, callback) { |
||
if (count === 0) { |
|||
new mw.Api().post({ |
|||
callback(); |
|||
action: 'wbcreateclaim', |
|||
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 |
|||
*/ |
|||
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 |
$.each(from.claims[name], function (i) { |
||
var |
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 |
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) { |
|||
}) |
|||
if (data.error && data.error.info) { |
|||
displayError(data.error.info); |
|||
} else { |
|||
displayError(data.error.info); |
|||
success(); |
|||
} |
|||
}).fail(function (data) { |
|||
} |
|||
displayError(data); |
|||
}) |
|||
}).always(log); |
|||
.fail(function (data) { |
|||
} |
|||
displayError(data); |
|||
}).always(log); |
|||
} |
|||
/** |
/** |
||
* Move a batch of items |
|||
*/ |
|||
function batchMover(items |
function batchMover(items) { |
||
displayProgress('Please wait...'); |
displayProgress('Please wait...'); |
||
$.each(items.from, function (i |
$.each(items.from, function (i) { |
||
var |
var x = items.from[i], |
||
sitelinks = x.sitelinks, |
|||
count = 0, |
|||
if (sitelinks === undefined) { |
|||
counter; |
|||
if (sitelinks === undefined) { |
|||
} |
|||
sitelinks = []; |
|||
if (items.from !== undefined) { |
|||
} |
|||
count = items.from.length; |
|||
if (items.from !== undefined) { |
|||
} |
|||
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 () { |
|||
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 () { |
|||
from: items.slice(1), |
|||
to: items[0] |
|||
}); |
|||
} else { |
} 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 |
|||
* Merge button |
|||
*/ |
|||
function |
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 { |
||
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 />', { |
||
' |
'id': formName, |
||
' |
'style': 'position: relative;' |
||
}).append( |
}).append( |
||
$('< |
$('<div />', { |
||
' |
'style': 'margin-top: 0.4em;', |
||
' |
'html': 'Merge into: ' |
||
}).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- |
'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( |
).append( |
||
$('< |
$('<div />').append( |
||
' |
$('<input />', { |
||
' |
'id': 'merge-send-to-rfd', |
||
' |
'name': 'merge-send-to-rfd', |
||
'type': 'checkbox' |
|||
) |
}) |
||
).append( |
).append( |
||
$('< |
$('<label />', { |
||
'id': 'merge-send-to-rfd-label', |
|||
' |
'for': 'merge-send-to-rfd', |
||
' |
'text': 'Request deletion for this item on RfD' |
||
}) |
|||
) |
|||
).append( |
).append( |
||
$('< |
$('<div />').append( |
||
' |
$('<input />', { |
||
' |
'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( |
).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(); |
||
$('#merge-queue-process').remove(); |
|||
mergePending(mw.config.get('wbEntityId')); |
|||
// launchDialog(); |
|||
}); |
|||
}); |
|||
} |
|||
$('#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));