Jump to content

User:Infrastruktur/cat-wd-linked.js

From Wikidata

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.
/*
 * cat-wd-linked: In category pages on Commons and Wikipedia, adds a menu item 'Category WD links'
 * that when clicked shows which subcategories are linked to Wikidata.
 *
 * It also shows which of the subcategories link to a Wikidata item that are missing claims for
 * P301:'category's main topic' or are missing P910:'topic's main category' claims.
 * It also shows which of the subcategories link to a Wikidata 'main' item, when it has
 * an associated 'category' item that it ought to be relinked to.
 *
 * By: User:Infrastruktur (@www.wikidata.org)
 *
 * License: CC-BY-SA 4.0
 *
 * Add to common.js on the wiki you want to use it on, or alternatively to your global.js
 * on metawiki:
 * mw.loader.load('//www.wikidata.org/w/index.php?title=User:Infrastruktur/cat-wd-linked.js&action=raw&ctype=text/javascript'); // [[User:Infrastruktur/cat-wd-linked.js]]
 *
 * Test categories:
 * https://commons.wikimedia.org/wiki/Category:Fortifications_by_type
 * https://en.wikipedia.org/wiki/Category:Fortifications_by_type
 * https://commons.wikimedia.org/wiki/Category:Defensive_walls_by_type
 * https://commons.wikimedia.org/wiki/Category:Museums_in_Cameroon
 * https://commons.wikimedia.org/wiki/Category:Buddhist_sculptures_by_subject
 *
 */

(function() {
'use strict';

const UA_STRING = 'Cat-WD-Linked/current (User:Infrastruktur)';
const max_anchor_uris = 2000; // to remain below the maximum query size
const max_cat_expansion = 0; // use this for testing purposes only

let on_commons = false, on_wikipedia = false;

function cats_query(uris, uri_prefix)
{
    console.assert(typeof uris === 'object');
    console.assert(typeof uri_prefix === 'string');

    const wdqs_endpoint = 'https://query.wikidata.org/sparql';
    const wdqs_query_prefix =
        'select distinct ?article (substr(str(?item_), 32) as ?item) ?missing_main_topic ?missing_main_cat ?relinking_needed ' +
        'where {' +
        '  values ?article_ {';
    const wdqs_query_postfix =
        '  }' +
        '  bind(uri(concat("' + uri_prefix + '", ?article_)) as ?article)' +
        '  optional {' +
        '    ?article schema:about ?item_ .' +
        '    optional {' +
        '      ?item_ wdt:P31 wd:Q4167836 .' +
        '      filter not exists { ?item_ wdt:P301 [] . }' +
        '      bind("yes" as ?missing_main_topic)' +
        '    }' +
        '    optional {' +
        '      filter not exists { ?item_ wdt:P31 wd:Q4167836 . }' +
        '      filter not exists { ?item_ wdt:P910 [] . }' +
        '      bind("yes" as ?missing_main_cat)' +
        '    }' +
        '    optional {' +
        '      ?item_ wdt:P910|(^wdt:P301) [] .' +
        '      filter not exists { ?item_ wdt:P31 wd:Q4167836 . }' +
        '      bind("yes" as ?relinking_needed)' +
        '    }' +
        '  }' +
        '}';
    const wdqs_query = wdqs_query_prefix + uris.map(uri => '"' + uri + '"').join(' ') + wdqs_query_postfix;
    const wdqs_fulluri = wdqs_endpoint + '?timeout=20';

    const promise = fetch(wdqs_fulluri, {
        method: 'POST',
        headers: {
            'Accept': 'application/sparql-results+json',
            'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
            'Api-User-Agent': UA_STRING
        },
        body: 'query=' + encodeURIComponent(wdqs_query)
    });

    return promise;
}

function check_cats(e)
{
    const plink_elem = this;
    const anchor_elems = document.querySelectorAll('div#mw-subcategories a[href]');
    const uri_prefix = 'https://' + mw.config.get('wgServerName');

    let anchor_uris = [];

    anchor_elems.forEach(function(elem) {
        const uri = elem.getAttribute('href');

        anchor_uris.push(uri);
        // console.log(uri);
    });

    if (anchor_uris.length > max_anchor_uris) {
        console.log('Requested to check ' + anchor_uris.length + ' subcategory links, which is too many. Checking only the first ' + max_anchor_uris + '.');
    } else {
        console.log('Requested to check ' + anchor_uris.length + ' subcategory links.');
    }

    cats_query(anchor_uris.slice(0, max_anchor_uris), uri_prefix)
    .then((response) => response.json())
    .then(function(data) {
        console.log('Fetched data for categories.');

        const results = data.results.bindings;

        data.results.bindings.forEach(function(row) {
            const uri = row.article.value.slice(uri_prefix.length);
            // console.log(uri);
            if ('item' in row) { // linked
                document.querySelectorAll('div#mw-subcategories a[href="' + uri + '"]').forEach(function(elem) {
                    // console.log(elem);
                    if (('relinking_needed' in row)) {
                        elem.setAttribute('style', 'color: #ff00ff');
                    } else if ((('missing_main_topic' in row) || ('missing_main_cat' in row))) {
                        elem.setAttribute('style', 'color: #7f7f00');
                    } else {
                        elem.setAttribute('style', 'color: #007f00');
                    }
                    const qid = row.item.value;
                    // console.log(qid);
                    let an = document.createElement('a');
                    an.setAttribute('href', 'https://www.wikidata.org/wiki/' + qid);
                    an.setAttribute('target', '_blank');
                    an.innerText = '(WD)';
                    // console.log(an);
                    let tn = document.createTextNode(' ');
                    elem.after(tn, an);
                });
            } else { // not linked
                document.querySelectorAll('div#mw-subcategories a[href="' + uri + '"]').forEach(function(elem) {
                    //console.log(elem);
                    elem.setAttribute('style', 'color: #7f0000');
                    let an = document.createElement('a');
                    an.setAttribute('href', 'https://www.wikidata.org/wiki/Special:Search');
                    an.setAttribute('target', '_blank');
                    an.innerText = '(WD search)';
                    // console.log(an);
                    let tn = document.createTextNode(' ');
                    elem.after(tn, an);
                });
            }
        });

    })
    .catch((error) => console.error('Cat-WD-Links query failed: ' + error));

    plink_elem.removeEventListener('click', check_cats);
    e.preventDefault();
}

function expand_cats(e)
{
    const plink_elem = this;
    const anchor_elems = document.querySelectorAll('div#mw-subcategories a.CategoryTreeToggle[aria-expanded="false"]');

    /* Expanding subcategories fires off one API call per subcategory to be expanded so
       this is something you ought not to do except for testing purposes. */
    if (anchor_elems.length >= max_cat_expansion) {
        console.log('Max category expansion exceeded with ' + anchor_elems.length + ' expandable subcategories.');
        plink_elem.removeEventListener('click', expand_cats);
    } else {
        for (let ai = anchor_elems.length - 1; ai > -1; ai--) {
            setTimeout(function() { anchor_elems.item(ai).click(); }, 0);
        }
    }

    e.preventDefault();
}

function main() {
    on_commons = mw.config.get('wgServerName') === 'commons.wikimedia.org';
    on_wikipedia = mw.config.get('wgServerName').match(/^[a-z]+\.wikipedia\.org$/);
    if (!(on_commons || on_wikipedia)) { return; }

    if (mw.config.get('wgNamespaceNumber') !== 14 ||
        mw.config.get('wgAction') !== 'view') { return; }

    let plink = mw.util.addPortletLink(
        'p-tb', '#', 'Category WD links', 't-cat-wd-links', 'Category WD links'
    );
    plink.addEventListener('click', check_cats);

    if (max_cat_expansion > 0) {
        let plink_expand = mw.util.addPortletLink(
            'p-tb', '#', 'Expand subcategories', 't-cat-expand', 'Expand subcategories'
        );
        plink_expand.addEventListener('click', expand_cats); // turn your cat into a chonk
    }
}

mw.loader.using([ 'mediawiki.util', 'mediawiki.api' ])
.then(function() {
    /* Wait for DOM ready */
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', function() {
            main();
        });
    } else {
        main();
    }
});

})();