MediaWiki:Gadget-Merge.js

From Wikidata
Revision as of 10:21, 23 May 2013 by Ebrahim (talk | contribs)
Jump to navigation Jump to search

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/*!
 * merge.js - Script to merge Wikidata items
 * 
 * Written by Ebraminio
 * modified by Ricordisamoa
 * 
 * Released under CC-Zero
 *
 */
/*jslint browser: true, regexp: true, indent: 2, unparam: true*/
/*global jQuery: false, mediaWiki: false, console: false*/
//<nowiki>
(function ($, mw) {
  'use strict';

  var formName = 'merge-form';

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

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

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

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

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

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

  function mergePending(id) {
    $.jStorage.set('merge-pending-id', id);
    mw.notify($.parseHTML('Merge.js has been started.<br />Now you can focus your browser on the other item and refresh the page.'));
  }

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

  /**
   * Logger function
   */
  function log(m) {
    console.log(m);
  }

  /**
   * Item editor
   */
  function editItem(id, data, summary, callback) {
    if (id === undefined) {
      displayError("the item's id is not defined");
      return;
    }
    data.id = undefined;
    new mw.Api().post({
      action: "wbeditentity",
      id: id,
      data: JSON.stringify(data),
      token: mw.user.tokens.get('editToken'),
      summary: summary
    }).always(log).done(callback);
  }

  /**
   * Item deleter
   */
  function deleteItem(id, reason, callback) {
    if (id === undefined) {
      displayError("the item's id is not defined");
      return;
    }
    if (canDelete() === false) {
      displayError("you're not an administrator");
      return;
    }
    displayProgress('Deleting the item...');
    new mw.Api().post({
      action: "delete",
      title: id,
      reason: reason,
      token: mw.user.tokens.get('editToken')
    }).always(log).done(callback);
  }

  /**
   * Clone just needed things
   */
  function cloneCleanItem(item) {
    return {
      id: item.id,
      sitelinks: $.extend({}, item.sitelinks),
      labels: $.extend({}, item.labels),
      descriptions: $.extend({}, item.descriptions)
    };
  }

  /**
   * Check equality of claims
   */
  function isEqualClaim(claimFrom, claimTo) {
    if (claimFrom.mainsnak === undefined && claimTo.mainsnak === undefined) {
      return true; // nothing is available to merge
    }

    if (claimFrom.mainsnak.datavalue === undefined || claimTo.mainsnak.datavalue === undefined) {
      throw new Error("Oh, this is not cool");
    }
    // this must be refactored
    return JSON.stringify(claimFrom.mainsnak.datavalue) === JSON.stringify(claimTo.mainsnak.datavalue);
  }

  /**
   * Add a claim
   */
  function addClaim(to, claim, callback) {
    new mw.Api().post({
      action: 'wbcreateclaim',
      entity: to.id,
      property: claim.mainsnak.property,
      snaktype: claim.mainsnak.snaktype,
      value: JSON.stringify(claim.mainsnak.datavalue.value),
      token: mw.user.tokens.get('editToken')
    }).always(log).done(callback);
  }

  /**
   * Abstraction to support multiple ajax calls
   */
  function AsyncCountDown(count, callback) {
    if (count === 0) {
      callback();
      return;
    }
    this.count = count;
    this.step = function () {
      this.count = this.count - 1;
      if (this.count === 0) {
        this.end();
      }
    };
    this.end = callback;
  }

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

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

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

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

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

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

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

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

  /**
   * Move a batch of items
   */
  function batchMover(items) {
    displayProgress('Please wait...');
    $.each(items.from, function (i) {
      var x = items.from[i],
        sitelinks = x.sitelinks,
        count = 0,
        counter;
      if (sitelinks === undefined) {
        sitelinks = [];
      }
      if (items.from !== undefined) {
        count = items.from.length;
      }
      counter = new AsyncCountDown(count, function () {
        window.location = mw.util.wikiGetlink(items.to.id);
      });
      moveItemContent(x, items.to, function () {
        $.jStorage.deleteKey('merge-pending-id'); // reset the Storage
        if (canDelete() && $('#merge-delete')[0].checked === true) {
          deleteItem(
            x.id.toUpperCase(),
            'Merged with [[' + items.to.id.toUpperCase() + ']] ([[User:Ebraminio/merge.js|merge.js]])',
            function () {
              counter.step();
            }
          );
        } else if ($('#merge-send-to-rfd')[0].checked === true) {
          requestDeletion(x.id.toUpperCase(), function () {
            counter.step();
          }, 'Merged with [[' + items.to.id.toUpperCase() + ']]');
        } else if ($('#merge-streamdelete')[0].checked === true) {
          requestStreamDeletion(x.id, function () {
            counter.step();
          }, items.to.id);
        } else {
          counter.step();
        }
      });
    });
  }

  /**
   * Merge button
   */
  function merge(itemsName) {
    getItems(itemsName, function (items) {
      var conflicts = detectConflicts(items),
        message;
      if ($.map(conflicts, function (x) { return x; }).length === 0) {
        items = sortItemsId(items); // try to sort by Qid
        batchMover({
          from: items.slice(1),
          to: items[0]
        });
      } else {
        message = $.map(conflicts, function (x, i) {
          return '<br />A conflict detected on ' + i.replace(/wiki$/, '') + ':' + $.map(x, function (y, j) {
            return ' [[' + x[j].id.toUpperCase() + ']] with [[' + i.replace(/wiki$/, '') + ':' + y.sitelinks[i].title + ']]';
          }).join(',');
        }).join('').replace(/\[\[(.*?)\]\]/g, function (x, y) {
          return '<a href="' + mw.util.wikiGetlink(y) + '">' + y + '</a>';
        });
        displayError(message);
      }
    });
  }

  /**
   * Dialog creator and launcher
   */
  function launchDialog(id) {
    if (id !== undefined) {
      id = '';
    } else {
      id = id.toUpperCase();
    }
    $('<div />', {
      'id': formName,
      'style': 'position: relative;'
    }).append(
      $('<div />', {
        'style': 'margin-top: 0.4em;',
        'html': 'Merge into: '
      }).append(
        $('<input />', {
          'id': 'merge-items',
          'style': 'padding: 1px; vertical-align: baseline;',
          'value': id
        })/*.entityselector({
          url: mw.util.wikiScript('api')
        }).on('entityselectorselect', function(e, ui) {
          $(this).attr("data-selected-id", ui.item.id);
        })*/
      ).append(
        $('<div />').append(
          $('<input />', {
            'id': 'merge-send-to-rfd',
            'name': 'merge-send-to-rfd',
            'type': 'checkbox'
          })
        ).append(
          $('<label />', {
            'id': 'merge-send-to-rfd-label',
            'for': 'merge-send-to-rfd',
            'text': 'Request deletion for this item on RfD'
          })
        )
      ).append(
        $('<div />').append(
          $('<input />', {
            'id': 'merge-streamdelete',
            'name': 'merge-streamdelete',
            'type': 'checkbox'
          })
        ).append(
          $('<label />', {
            'id': 'merge-streamdelete-label',
            'for': 'merge-streamdelete',
            'html': 'Request <a href="' + mw.util.wikiGetlink('User:Ricordisamoa/StreamDelete') + '">StreamDeletion</a> for this item (experimental)'
          })
        )
      ).append(!canDelete() ? '' : $('<div />').append(
        $('<input />', {
          'id': 'merge-delete',
          'name': 'merge-delete',
          'type': 'checkbox'
        }).append(
          $('<label />', {
            'id': 'merge-delete-label',
            'for': 'merge-delete',
            'text': 'Try to automatically delete this item after merge (only admins)'
          })
        )
      ))
    ).dialog({
      width: 500,
      autoOpen: false,
      title: 'Merge Wizard',
      modal: true,
      buttons: [{
        text: 'Close',
        specialButton: 'cancel',
        click: function () {
          $(formName).remove();
        }
      }, {
        text: 'Merge',
        specialButton: 'proceed',
        click: function () {
          // $('#merge-items').attr("data-selected-id") must be used if entityselector is enabled
          merge([$('#merge-items').val(), mw.config.get('wbEntityId')]);
        }
      }]
    }).dialog('open');
  }

  // Initialization
  if (mw.config.get('wgNamespaceNumber') === 0 && mw.config.get('wgAction') === 'view') {
    mw.loader.using(['jquery.ui.dialog', 'jquery.jStorage', 'jquery.spinner'], function() {
      $(function() {
        if ($.jStorage.get('merge-pending-id') !== null && $.jStorage.get('merge-pending-id') !== '' && $.jStorage.get('merge-pending-id').toLowerCase() !==  mw.config.get('wbEntityId').toLowerCase()) {
          $('#merge-queue-process').remove();
          $('<img>')
            .attr('src', '//upload.wikimedia.org/wikipedia/commons/thumb/1/10/Pictogram_voting_merge.svg/28px-Pictogram_voting_merge.svg.png')
            .wrap('<a>').parent()
            .attr({
              'href': '#',
              'title': 'process the merge'
            })
            .click(function (event) {
              event.preventDefault();
              launchDialog($.jStorage.get('merge-pending-id'));
            })
            .wrap('<li>').parent()
            .attr('id', 'merge-queue-process')
            .prependTo('#p-views ul');
        }
        $('#ca-merge').remove();
        $(mw.util.addPortletLink('p-cactions', '#', 'Merge it with...', 'ca-merge', 'Merge this item into another and send this to RfD'))
          .click(function (event) {
            event.preventDefault();
            $('#merge-queue-process').remove();
            mergePending(mw.config.get('wbEntityId'));
            // launchDialog();
          });
      });
    });
  }

}(jQuery, mediaWiki));