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
(2 intermediate revisions by the same user not shown)
Line 2: Line 2:
* merge.js - Script to merge Wikidata items
* merge.js - Script to merge Wikidata items
*
*
* Written by Ebraminio
* Written by Ebraminio, and Ricordisamoa
* modified by Ricordisamoa
*
*
* Released under CC-Zero
* Released under CC-Zero
Line 35: Line 34:
'html': 'Error: ' + error
'html': 'Error: ' + error
}));
}));
}

/**
* Check if user has right to delete an item
*/
function canDelete() {
return wgUserGroups.indexOf("sysop") !== -1;
}
}


Line 109: Line 115:
token: mw.user.tokens.get('editToken'),
token: mw.user.tokens.get('editToken'),
summary: summary
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);
}).always(log).done(callback);
}
}
Line 143: Line 170:
*/
*/
function addClaim(to, claim, callback) {
function addClaim(to, claim, callback) {
console.log(to, claim, callback);
new mw.Api().post({
new mw.Api().post({
action: 'wbcreateclaim',
action: 'wbcreateclaim',
Line 150: Line 178:
value: JSON.stringify(claim.mainsnak.datavalue.value),
value: JSON.stringify(claim.mainsnak.datavalue.value),
token: mw.user.tokens.get('editToken')
token: mw.user.tokens.get('editToken')
}).always(log).done(callback);
}).always(log).done(function () {
if (claim.references !== undefined) {
new mw.Api().post({
statement: claim.id,
action: 'wbsetreference',
snaks: JSON.stringify(claim.references[0].snaks),
reference: claim.references[0].hash,
token: mw.user.tokens.get('editToken')
}).always(log).done(function () {
callback();
});
} else {
callback();
}
});
}
}


Line 273: Line 315:
});
});
moveItemContent(x, items.to, function () {
moveItemContent(x, items.to, function () {
if ($('#merge-send-to-rfd')[0].checked === true) {
if (canDelete() && $('#merge-delete')[0].checked === true) {
deleteItem(x.id.toUpperCase(), 'Merged with [[' + items.to.id.toUpperCase() + ']] ([[User:Ricordisamoa/merge.js|merge.js]])', function () {
counter.step();
});
} else if ($('#merge-send-to-rfd')[0].checked === true) {
requestDeletion(x.id.toUpperCase(), function () {
requestDeletion(x.id.toUpperCase(), function () {
counter.step();
counter.step();
Line 330: Line 376:
$(this).attr("data-selected-id", ui.item.id);
$(this).attr("data-selected-id", ui.item.id);
})*/
})*/
).append('<br />').append(
$('<input />', {
'id': 'merge-send-to-rfd',
'name': 'merge-send-to-rfd',
'type': 'checkbox'
})
).append(
).append(
$('<label />', {
$('<div />').append(
'for': 'merge-send-to-rfd',
$('<input />', {
'text': 'Request deletion for this item on RfD'
'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-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({
).dialog({

Revision as of 19:32, 22 May 2013

/*!
 * merge.js - Script to merge Wikidata items
 * 
 * Written by Ebraminio, and Ricordisamoa
 * 
 * Released under CC-Zero
 *
 */
//<nowiki>
(function ($, mw) {
'use strict';

var formName = 'merge-form';

/**
 * Display progress on form dialog
 */
function displayProgress(message) {
  $('#' + formName + ' div').hide();
  $('#' + formName).append($('<div />', {
    'style': 'text-align:center; margin:3em 0;',
    'html': message + '<br/><img src="//upload.wikimedia.org/wikipedia/commons/4/42/Loading.gif" />'
  }));
}

/**
 * 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 user has right to delete an item
 */
function canDelete() {
  return 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, item) {
    if (item.sitelinks !== undefined) {
      $.each(item.sitelinks, function (i, x) {
        if (all[i] !== undefined) {
          if (conflicts[i] === undefined) {
            conflicts[i] = [all[i]];
          }
          conflicts[i].push(item);
        }
        all[i] = item;
      });
    }
  });
  return conflicts;
}

/**
 * Sort items by their serialized size. Useful for detecting eligible item to merged into.
 * NOT USED CURRENTLY
 */
function sortItemsDesc(items) {
  return $.map($.map(items, function (item) {
   return {
     item: item,
     size: JSON.stringify(item).length
   };
  }).sort(function (x, y) { return y.size - x.size; }), 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) {
  console.log(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(function () {
    if (claim.references !== undefined) {
      new mw.Api().post({
        statement: claim.id,
        action: 'wbsetreference',
        snaks: JSON.stringify(claim.references[0].snaks),
        reference: claim.references[0].hash,
        token: mw.user.tokens.get('editToken')
      }).always(log).done(function () {
        callback();
      });
    } else {
      callback();
    }
   });
}

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

/**
 * 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;
}

/**
 * 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:Ebraminio/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);
}

/**
 * Move a batch of items
 */
function batchMover(items, callback) {
  displayProgress('Please wait...');
  $.each(items.from, function (i, x) {
    var sitelinks = x.sitelinks,
        count = 0;
    if (sitelinks === undefined) {
      sitelinks = [];
    }
    if (items.from !== undefined) {
      count = items.from.length;
    }
    var counter = new AsyncCountDown(items.from.length, function () {
      window.location = mw.util.wikiGetlink(items.to.id);
    });
    moveItemContent(x, items.to, function () {
      if (canDelete() && $('#merge-delete')[0].checked === true) {
         deleteItem(x.id.toUpperCase(), 'Merged with [[' + items.to.id.toUpperCase() + ']] ([[User:Ricordisamoa/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 {
         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) {
      // var items = sortItemsDesc(items); not sorted, first item is destination
      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() {
  $('<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;'
      })/*.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-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: {
      "Merge": function () {
        merge([$('#merge-items').val()/*.attr("data-selected-id")*/, mw.config.get('wgPageName')]);
      }
    },
    "close": function () {
      $(formName).remove();
    }
  }).dialog('open');
}

// Initialization
if (mw.config.get('wgNamespaceNumber') === 0 && mw.config.get('wgAction') === 'view') {
  mw.loader.using('jquery.ui.dialog', function() {
    $(function() {
      $('#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();
          launchDialog();
        });
    });
  });
}

}(jQuery, mediaWiki));