User:JesseW/conflicting superclass warnings.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)
- Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
- Opera: Press Ctrl-F5.
( function ( mw ) {
// TODO: Ideal functionality:
// When a new value is selected for either the instance-of or subclass-of
// properties, BEFORE it is saved, the system should look up
// the existing superclasses (via instance-of if that is what is being
// changed, or subclass-of otherwise, in both cases handling multiple existing values)
// and the superclasses of the newly selected value, and check for conflicts.
// If conflicts are found, display a notification of the appropriate form
// (always ending with: "Is this right?" or "Please check this", or something; and having the Q numbers shown and linked):
// - This is already a "natural object" (Q123). Adding "instance of"
// "boat" (Q123) makes it also an "artificial entity" (Q123).
// - This is already a subclass of "natural object" (Q123). Adding "subclass of"
// "boat" (Q123) makes it a subclass of "artificial entity" (Q123), too.
// - This is currently a "natural object". Changing "instance of" "tree" to
// "rocket" makes it both a "natural object" and an "artificial entity".
// - This is currently a "natural object". Changing "instance of" "tree" to
// "rocket" makes it instead an "artificial entity".
// - This is currently a subclass of "natural object". Changing "subclass of"
// "tree" to "rocket" makes it a subclass of both "natural object" and "artificial entity".
// - This is currently a subclass of "natural object". Changing "subclass of"
// "tree" to "rocket" makes it instead a subclass of "artificial entity".
// Currently implemented features include checking subclass-of and instance-of
// when a replacement value is selected. Additions are not checked, and it always uses
// the original value from when the page was loaded (so multiple edits
// without reloading the page will give wrong results). It shows all the
// conflicts, but doesn't distinguish all the cases listed above.
var CONFLICTS = [
[ 26907166, 58416391], // temporal entity, spatial entity
[112276019, 7048977], // physical entity, non-physical entity
[ 14897293, 96196524], // fictional entity, current entity
[ 3249551, 4406616], // process, concrete object
[ 54989186, 58416391], // mental state, spatial entity
[ 35145263, 35145743], // natural geographic object, human-made geographic object
// The following are listed as "disjoint union of" "entity"
// https://www.wikidata.org/wiki/Q35120#P2738
[ 23958946, 23960977], // individual entity, (meta)class
[ 15893266, 96196524], // former entity, current entity
[ 29651224, 16686448], // natural object, artificial entity
[ 488383, 937228, 3249551], // object, property, process
[ 30241068, 3799040], // observable entity, unobservable entity
];
var api;
var maxdepth = 20;
var labelCache = {};
function getLabels(itemIds) {
var idsToLoad = $.grep(itemIds, function (i) {return !labelCache[i]; })
.map(function (x) { return 'Q' + x; }).join('|');
var args = {action: 'wbgetentities', props: ['labels'], languages: 'en',
ids: idsToLoad};
return (idsToLoad ? api.get(args) : $.Deferred().resolve())
.then(function (data) {
var rtn = {};
itemIds.forEach(function (x) {
fresh = ((data || {}).entities || {})['Q' + x];
if (fresh) {
labelCache[x] = fresh.labels.en.value;
console.log('added to label cache ' + x + " = " + labelCache[x]);
}
rtn[x] = labelCache[x];
});
console.log('labels returned: ' + JSON.stringify(rtn));
return rtn;
});
}
var claimCache={};
function getClaims(itemId, property) {
var key = itemId + ":" + property;
if (claimCache[key]) {
console.log('got claims from cache for: ' + key);
return $.Deferred().resolve().then(function () {
return claimCache[key];
});
}
//console.log('making getClaims api call: ' + key);
return api.get( {
action: 'wbgetclaims',
entity: 'Q' + itemId,
property: property,
props: [ ]
} ).then( function ( data ) {
claimCache[key] = (data.claims[property] || [])
.filter(function (x) { return x.mainsnak.datavalue; })
.map(function (x) { return x.mainsnak.datavalue.value['numeric-id'];});
console.log('added to claim cache ' + key + " = " + claimCache[key]);
return claimCache[key];
} );
}
var superclassesCache = {};
function getSuperclasses(rootItemId) {
if (superclassesCache[rootItemId]) {
console.log('got superclasses from cache for ' + rootItemId);
return $.Deferred().resolve().then(function () {
return superclassesCache[rootItemId];
});
}
console.log('getting superclasses for ' + rootItemId);
var data = {};
var children={};
var visiting={};
function visit(itemId, depth) {
if (visiting[itemId]) {
return $.Deferred().resolve(depth);
}
//console.log("visiting " + itemId + " at depth " + depth);
visiting[itemId] = true;
if (children[itemId]) {
visiting[itemId] = false;
return $.Deferred().resolve(depth + 1);
} else if (depth <= maxdepth) {
return getClaims(itemId, 'P279').then(function (childIds) {
childIds.forEach(function (childId) {
data[childId] = true;
});
return $.when.apply(null, childIds.map(function (childId) {
return visit(childId, depth +1);
})).then(function () {
children[itemId] = childIds;
visiting[itemId] = false;
return depth + 1;
});
});
}
}
return visit(rootItemId, 1).then(function () {
superclassesCache[rootItemId] = data;
return data;
});
}
function checkForConflicts(n, o) {
var problems = {"old": {}, "new": {}, "has": false};
CONFLICTS.forEach(function (conflict) {
conflict.forEach(function (q) {
//console.log('checking ' + q + ": " + o[q] + "; " + n[q]);
if (o[q]) {
conflict.forEach(function (q2) {
//console.log('checking for conflict ' + q2 + ": " + o[q2] + "; " + n[q2]);
if (q != q2 && n[q2]) {
problems.has = true;
problems.old[q] = true;
problems.new[q2] = true;
}
});
}
});
});
return problems;
}
function humanizeList(list) {
// Adapted from https://github.com/johno/humanize-list/blob/master/index.js
var listLength = list.length;
if (listLength === 1) {
return list[0];
}
var humanizedList = '';
for (var i = 0; i < listLength; i++) {
if (i === listLength - 1) {
humanizedList += ' and ';
} else if (i !== 0) {
humanizedList += ', ';
}
humanizedList += list[i];
}
return humanizedList;
}
function getOriginalId(element) {
return $("a", element)[0].href.split("/").slice(-1)[0].substr(1);
}
function keys(obj) {
return $.map(obj, function (v, k) {return k;});
}
function makeDisplayError(originalId, newId, propertyLabel) {
function displayError(problems) {
var itemsToLabel = [originalId, newId].concat(keys(problems.old), keys(problems.new));
getLabels(itemsToLabel).then(function (labels) {
function labelAndLink(unused, id) {
return "\"<a href=\"https://www.wikidata.org/wiki/Q" + id + "\">" + labels[id] + "</a>\"";
}
var message = $("<span>This is currently " + propertyLabel + " of " +
humanizeList($.map(problems.old, labelAndLink)) + "." +
" Changing " + labelAndLink(null, originalId) + " to " + labelAndLink(null, newId) +
" makes it " + propertyLabel + " of " +
humanizeList($.map(problems.new, labelAndLink)) +
". Is that right?</span>");
mw.notify(message, {autoHide: false});
});
}
return displayError;
}
function makeCheckFunction(displayError) {
return function (n, o) {
problems = checkForConflicts(n, o);
if (problems.has) {
displayError(problems);
} else {
mw.notify("No problems found.");
}
};
}
mw.loader.using('mediawiki.api', function() {
//console.log('modules loaded');
api = new mw.Api();
//doTest();
var currentPageId=$(".wikibase-title-id").text().split(")")[0].substr(2);
// Possibility for checking added values:
// $("#P31").on("entityselectorselected", ".wikibase-snakview-value",
$("#P279 .wikibase-snakview-value").each(function (index, element) {
var originalId = getOriginalId(element);
$(element).on("entityselectorselected", function (event, qid) {
var newId=qid.substr(1);
$.when(getSuperclasses(newId), getSuperclasses(currentPageId))
.then(makeCheckFunction(makeDisplayError(originalId, newId, "a subclass")));
});
});
$("#P31 .wikibase-snakview-value").each(function (index, element) {
var originalId = getOriginalId(element);
$(element).on("entityselectorselected", function (event, qid) {
var newId=qid.substr(1);
var currentClasses = getClaims(currentPageId, "P31").then(function (claims) {
return $.when.apply(null, claims.map(getSuperclasses)).then(function () {
all_superclasses = {};
$.each(arguments, function (idx, ss) {
$.each(ss, function (s) {
// getLabels([s]).then(function (labels) {
// console.log('adding current instance of ' + labels[s] + "(" + s + ")");
// });
all_superclasses[s] = true;
});
});
return all_superclasses;
});
});
$.when(getSuperclasses(newId), currentClasses)
.then(makeCheckFunction(makeDisplayError(originalId, newId, "an instance")));
});
});
});
function doTest() {
// probably broken at this point
getSuperclasses(35872).then(function (data) {
//console.log(data);
getLabels($.map(data, function (v,k) {return k;})).then(console.log);
getSuperclasses(10884).then(function (data2) {
getLabels($.map(data2, function (v,k) {return k;})).then(console.log);
console.log(checkForConflicts(data, data2));
});
});
}
}
( mw )
);
// // Random commented-out leftovers:
//mw.hook('wikibase.statement.saved').add(do_check);
//mw.hook( 'wikibase.entityPage.entityView.rendered' ).add( init );
// function getSuperclassesOfStatementValue(s) {
// return getSuperclasses(s._claim._mainSnak._value._serialization.substr(1));
// }
// function do_check(entityId, statementId, oldS, newS) {
// thing={oldS: oldS, newS:newS};
// // A change in subclassOf
// if (newS._claim._mainSnak._propertyId == "P279") {
// if (oldS != null) {
// $.when(
// getSuperclassesOfStatementValue(newS),
// getSuperclassesOfStatementValue(oldS)
// ).then(function (n, o) {
// thing.newSC = n;
// thing.oldSC = o;
// thing.problems = checkForConflicts(n, o);
// if (thing.problems.length) {
// getLabels(thing.problems[0]).then(function (labels) {
// mw.notify("Problems! " + labels, {autoHide: false});
// });
// }
// });
// }
// }
// }
// getClaims(mw.config.values.wbEntityId.substr(1), 'P31', function (childIds) {
// thing.instanceOf = childIds.map(function (childId) { return getSuperclasses(childId); });
// });
// thing.subclassOf = getSuperclasses(mw.config.values.wbEntityId.substr(1));