Fixes duplicates view for async DB

It's way too slow, though, since the whole list is regenerated after
merging.

Fixes #519

Also:

- The arguments to Zotero.Item.prototype.clone() have changed, and it no
  longer takes an existing item or copies primary data. To create an
  in-memory copy of an item, use the new Zotero.Item.prototype.copy().

- Zotero.Item.prototype.getUsedFields() now gets in-memory fields rather
  than the fields in the database
This commit is contained in:
Dan Stillman 2014-10-10 04:35:45 -04:00
parent 15d28014ed
commit 5c94119c70
11 changed files with 305 additions and 377 deletions

View File

@ -71,7 +71,7 @@
} }
// Clone object so changes in merge pane don't affect it // Clone object so changes in merge pane don't affect it
this._leftpane.ref = val.clone(true); this._leftpane.ref = val.copy();
this._leftpane.original = val; this._leftpane.original = val;
]]> ]]>
</setter> </setter>
@ -97,7 +97,7 @@
} }
// Clone object so changes in merge pane don't affect it // Clone object so changes in merge pane don't affect it
this._rightpane.ref = val.clone(true); this._rightpane.ref = val.copy();
this._rightpane.original = val; this._rightpane.original = val;
} }

View File

@ -130,16 +130,21 @@ var Zotero_Duplicates_Pane = new function () {
// Add master item's values to the beginning of each set of // Add master item's values to the beginning of each set of
// alternative values so that they're still available if the item box // alternative values so that they're still available if the item box
// modifies the item // modifies the item
var diff = item.multiDiff(_otherItems, _ignoreFields); Zotero.spawn(function* () {
if (diff) { var diff = yield item.multiDiff(_otherItems, _ignoreFields);
var itemValues = item.serialize() if (diff) {
for (var i in diff) { let itemValues = yield item.toJSON();
diff[i].unshift(itemValues.fields[i]); for (let i in diff) {
diff[i].unshift(itemValues[i] !== undefined ? itemValues[i] : '');
}
itembox.fieldAlternatives = diff;
} }
itembox.fieldAlternatives = diff;
}
itembox.item = item.clone(true); var newItem = yield item.copy();
yield newItem.loadItemData();
yield newItem.loadCreators();
itembox.item = newItem;
});
} }

View File

@ -1067,22 +1067,15 @@ Zotero.Attachments = new function(){
throw ("Attachment is already in library " + libraryID); throw ("Attachment is already in library " + libraryID);
} }
var newAttachment = new Zotero.Item('attachment'); var newAttachment = attachment.clone(libraryID);
newAttachment.libraryID = libraryID;
// Link mode needs to be set when saving new attachment
newAttachment.attachmentLinkMode = linkMode;
if (attachment.isImportedAttachment()) { if (attachment.isImportedAttachment()) {
// Attachment path isn't copied over by clone() if libraryID is different // Attachment path isn't copied over by clone() if libraryID is different
newAttachment.attachmentPath = attachment.attachmentPath; newAttachment.attachmentPath = attachment.attachmentPath;
} }
// DEBUG: save here because clone() doesn't currently work on unsaved tagged items
var id = newAttachment.save();
newAttachment = Zotero.Items.get(id);
attachment.clone(false, newAttachment);
if (parentItemID) { if (parentItemID) {
newAttachment.setSource(parentItemID); newAttachment.parentID = parentItemID;
} }
newAttachment.save(); yield newAttachment.save();
// Copy over files if they exist // Copy over files if they exist
if (newAttachment.isImportedAttachment() && attachment.getFile()) { if (newAttachment.isImportedAttachment() && attachment.getFile()) {
@ -1091,7 +1084,7 @@ Zotero.Attachments = new function(){
Zotero.File.copyDirectory(dir, newDir); Zotero.File.copyDirectory(dir, newDir);
} }
newAttachment.addLinkedItem(attachment); yield newAttachment.addLinkedItem(attachment);
return newAttachment.id; return newAttachment.id;
}); });

View File

@ -1567,23 +1567,15 @@ Zotero.CollectionTreeView.prototype.drop = Zotero.Promise.coroutine(function* (r
return Zotero.Attachments.copyAttachmentToLibrary(item, targetLibraryID); return Zotero.Attachments.copyAttachmentToLibrary(item, targetLibraryID);
} }
// Create new unsaved clone item in target library // Create new clone item in target library
var newItem = new Zotero.Item(item.itemTypeID); var newItem = item.clone(targetLibraryID, false, !Zotero.Prefs.get('groups.copyTags'));
newItem.libraryID = targetLibraryID; var newItemID = yield newItem.save();
// DEBUG: save here because clone() doesn't currently work on unsaved tagged items
var id = yield newItem.save();
newItem = yield Zotero.Items.getAsync(id);
yield item.clone(false, newItem, false, !Zotero.Prefs.get('groups.copyTags'));
yield newItem.save();
//var id = newItem.save();
//var newItem = Zotero.Items.get(id);
// Record link // Record link
yield newItem.addLinkedItem(item); yield newItem.addLinkedItem(item);
var newID = id;
if (item.isNote()) { if (item.isNote()) {
return newID; return newItemID;
} }
// For regular items, add child items if prefs and permissions allow // For regular items, add child items if prefs and permissions allow
@ -1593,14 +1585,9 @@ Zotero.CollectionTreeView.prototype.drop = Zotero.Promise.coroutine(function* (r
var noteIDs = item.getNotes(); var noteIDs = item.getNotes();
var notes = yield Zotero.Items.getAsync(noteIDs); var notes = yield Zotero.Items.getAsync(noteIDs);
for each(var note in notes) { for each(var note in notes) {
var newNote = new Zotero.Item('note'); let newNote = note.clone(targetLibraryID);
newNote.libraryID = targetLibraryID; newNote.parentID = newItemID;
// DEBUG: save here because clone() doesn't currently work on unsaved tagged items yield newNote.save()
var id = yield newNote.save();
newNote = yield Zotero.Items.getAsync(id);
yield note.clone(false, newNote);
newNote.parentID = newItem.id;
yield newNote.save();
yield newNote.addLinkedItem(note); yield newNote.addLinkedItem(note);
} }

View File

@ -421,73 +421,64 @@ Zotero.DataObjects = function (object, objectPlural, id, table) {
/** /**
* @param {Object} data1 Serialized copy of first object * @param {Object} data1 - JSON of first object
* @param {Object} data2 Serialized copy of second object * @param {Object} data2 - JSON of second object
* @param {Array} diff Empty array to put diff data in * @param {Array} diff - Empty array to put diff data in
* @param {Boolean} [includeMatches=false] Include all fields, even those * @param {Boolean} [includeMatches=false] - Include all fields, even those
* that aren't different * that aren't different
*/ */
this.diff = function (data1, data2, diff, includeMatches) { this.diff = function (data1, data2, diff, includeMatches) {
diff.push({}, {}); diff.push({}, {});
var numDiffs = 0; var numDiffs = 0;
var subs = ['primary', 'fields']; var skipFields = ['collectionKey', 'itemKey', 'searchKey'];
var skipFields = ['collectionID', 'creatorID', 'itemID', 'searchID', 'tagID', 'libraryID', 'key'];
for each(var sub in subs) { for (var field in data1) {
diff[0][sub] = {}; if (skipFields.indexOf(field) != -1) {
diff[1][sub] = {}; continue;
for (var field in data1[sub]) {
if (skipFields.indexOf(field) != -1) {
continue;
}
if (!data1[sub][field] && !data2[sub][field]) {
continue;
}
var changed = !data1[sub][field] || !data2[sub][field] ||
data1[sub][field] != data2[sub][field];
if (includeMatches || changed) {
diff[0][sub][field] = data1[sub][field] ?
data1[sub][field] : '';
diff[1][sub][field] = data2[sub][field] ?
data2[sub][field] : '';
}
if (changed) {
numDiffs++;
}
} }
// DEBUG: some of this is probably redundant if (data1[field] === false && (data2[field] === false || data2[field] === undefined)) {
for (var field in data2[sub]) { continue;
if (skipFields.indexOf(field) != -1) { }
continue;
}
if (diff[0][sub][field] != undefined) { var changed = data1[field] !== data2[field];
continue;
}
if (!data1[sub][field] && !data2[sub][field]) { if (includeMatches || changed) {
continue; diff[0][field] = data1[field] !== false ? data1[field] : '';
} diff[1][field] = (data2[field] !== false && data2[field] !== undefined)
? data2[field] : '';
}
var changed = !data1[sub][field] || !data2[sub][field] || if (changed) {
data1[sub][field] != data2[sub][field]; numDiffs++;
}
}
if (includeMatches || changed) { // DEBUG: some of this is probably redundant
diff[0][sub][field] = data1[sub][field] ? for (var field in data2) {
data1[sub][field] : ''; if (skipFields.indexOf(field) != -1) {
diff[1][sub][field] = data2[sub][field] ? continue;
data2[sub][field] : ''; }
}
if (changed) { if (diff[0][field] !== undefined) {
numDiffs++; continue;
} }
if (data2[field] === false && (data1[field] === false || data1[field] === undefined)) {
continue;
}
var changed = data1[field] !== data2[field];
if (includeMatches || changed) {
diff[0][field] = (data1[field] !== false && data1[field] !== undefined)
? data1[field] : '';
diff[1][field] = data2[field] !== false ? data2[field] : '';
}
if (changed) {
numDiffs++;
} }
} }

View File

@ -232,7 +232,7 @@ Zotero.Item.prototype.getField = function(field, unformatted, includeBaseMapped)
); );
} }
var value = value ? value : ''; value = value ? value : '';
if (!unformatted) { if (!unformatted) {
// Multipart date fields // Multipart date fields
@ -251,18 +251,11 @@ Zotero.Item.prototype.getField = function(field, unformatted, includeBaseMapped)
* @return {Integer{}|String[]} * @return {Integer{}|String[]}
*/ */
Zotero.Item.prototype.getUsedFields = Zotero.Promise.coroutine(function* (asNames) { Zotero.Item.prototype.getUsedFields = Zotero.Promise.coroutine(function* (asNames) {
if (!this.id) { this._requireData('itemData');
return [];
} return Object.keys(this._itemData)
var sql = "SELECT fieldID FROM itemData WHERE itemID=?"; .filter(id => this._itemData[id] !== false)
if (asNames) { .map(id => asNames ? Zotero.ItemFields.getName(id) : parseInt(id));
sql = "SELECT fieldName FROM fields WHERE fieldID IN (" + sql + ")";
}
var fields = yield Zotero.DB.columnQueryAsync(sql, this._id);
if (!fields) {
return [];
}
return fields;
}); });
@ -1581,7 +1574,8 @@ Zotero.Item.prototype.save = Zotero.Promise.coroutine(function* (options) {
for (let i=0; i<toAdd.length; i++) { for (let i=0; i<toAdd.length; i++) {
let tag = toAdd[i]; let tag = toAdd[i];
let tagID = yield Zotero.Tags.getIDFromName(this.libraryID, tag.tag, true); let tagID = yield Zotero.Tags.getIDFromName(this.libraryID, tag.tag, true);
let sql = "INSERT INTO itemTags (itemID, tagID, type) VALUES (?, ?, ?)"; // "OR REPLACE" allows changing type
let sql = "INSERT OR REPLACE INTO itemTags (itemID, tagID, type) VALUES (?, ?, ?)";
yield Zotero.DB.queryAsync(sql, [this.id, tagID, tag.type ? tag.type : 0]); yield Zotero.DB.queryAsync(sql, [this.id, tagID, tag.type ? tag.type : 0]);
Zotero.Notifier.trigger('add', 'item-tag', this.id + '-' + tag.tag); Zotero.Notifier.trigger('add', 'item-tag', this.id + '-' + tag.tag);
} }
@ -3248,31 +3242,35 @@ Zotero.Item.prototype.setTags = function (tags) {
/** /**
* Add a single manual tag to the item. If an automatic tag with the same name already exists, * Add a single tag to the item. If type is 1 and an automatic tag with the same name already
* replace it with a manual one. * exists, replace it with a manual one.
* *
* A separate save() is required to update the database. * A separate save() is required to update the database.
* *
* @param {String} name * @param {String} name
* @param {Number} [type=0]
*/ */
Zotero.Item.prototype.addTag = function (name) { Zotero.Item.prototype.addTag = function (name, type) {
type = type ? parseInt(type) : 0;
var changed = false; var changed = false;
var tags = this.getTags(); var tags = this.getTags();
for (let i=0; i<tags.length; i++) { for (let i=0; i<tags.length; i++) {
let tag = tags[i]; let tag = tags[i];
if (tag.tag === name) { if (tag.tag === name) {
if (!tag.type) { if (tag.type == type) {
Zotero.debug("Tag '" + name + "' already exists on item " + this.libraryKey); Zotero.debug("Tag '" + name + "' already exists on item " + this.libraryKey);
return false; return false;
} }
tag.type = 0; tag.type = type;
changed = true; changed = true;
break; break;
} }
} }
if (!changed) { if (!changed) {
tags.push({ tags.push({
tag: name tag: name,
type: type
}); });
} }
this.setTags(tags); this.setTags(tags);
@ -3706,24 +3704,25 @@ Zotero.Item.prototype.diff = function (item, includeMatches, ignoreFields) {
* *
* Currently compares only item data, not primary fields * Currently compares only item data, not primary fields
*/ */
Zotero.Item.prototype.multiDiff = function (otherItems, ignoreFields) { Zotero.Item.prototype.multiDiff = Zotero.Promise.coroutine(function* (otherItems, ignoreFields) {
var thisData = this.serialize(); var thisData = yield this.toJSON();
var alternatives = {}; var alternatives = {};
var hasDiffs = false; var hasDiffs = false;
for each(var otherItem in otherItems) { for (let i = 0; i < otherItems.length; i++) {
var diff = []; let otherItem = otherItems[i];
var otherData = otherItem.serialize(); let diff = [];
var numDiffs = Zotero.Items.diff(thisData, otherData, diff); let otherData = yield otherItem.toJSON();
let numDiffs = Zotero.Items.diff(thisData, otherData, diff);
if (numDiffs) { if (numDiffs) {
for (var field in diff[1].fields) { for (let field in diff[1]) {
if (ignoreFields && ignoreFields.indexOf(field) != -1) { if (ignoreFields && ignoreFields.indexOf(field) != -1) {
continue; continue;
} }
var value = diff[1].fields[field]; var value = diff[1][field];
if (!alternatives[field]) { if (!alternatives[field]) {
hasDiffs = true; hasDiffs = true;
@ -3742,143 +3741,41 @@ Zotero.Item.prototype.multiDiff = function (otherItems, ignoreFields) {
} }
return alternatives; return alternatives;
} });
/** /**
* Returns an unsaved copy of the item * Returns an unsaved copy of the item
* *
* @param {Boolean} [includePrimary=false] * @param {Number} [libraryID] - libraryID of the new item, or the same as original if omitted
* @param {Zotero.Item} [newItem=null] Target item for clone (used to pass a saved * @param {Boolean} [skipTags=false] - Skip tags
* item for duplicating items with tags)
* @param {Boolean} [unsaved=false] Skip properties that require a saved object (e.g., tags)
* @param {Boolean} [skipTags=false] Skip tags (implied by 'unsaved')
*/ */
Zotero.Item.prototype.clone = function(includePrimary, newItem, unsaved, skipTags) { Zotero.Item.prototype.clone = function(libraryID, skipTags) {
Zotero.debug('Cloning item ' + this.id); Zotero.debug('Cloning item ' + this.id);
if (includePrimary && newItem) { if (libraryID !== undefined && libraryID !== null && typeof libraryID !== 'number') {
throw ("includePrimary and newItem parameters are mutually exclusive in Zotero.Item.clone()"); throw new Error("libraryID must be null or an integer");
} }
if (unsaved) { this._requireData('primaryData');
skipTags = true;
if (libraryID === undefined || libraryID === null) {
libraryID = this.libraryID;
} }
var sameLibrary = libraryID == this.libraryID;
Zotero.DB.beginTransaction(); var newItem = new Zotero.Item;
newItem.setType(this.itemTypeID);
// TODO: get rid of serialize() call var fieldIDs = this.getUsedFields();
var obj = this.serialize(); for (let i = 0; i < fieldIDs.length; i++) {
let fieldID = fieldIDs[i];
var itemTypeID = this.itemTypeID; newItem.setField(fieldID, this.getField(fieldID));
if (newItem) {
var sameLibrary = newItem.libraryID == this.libraryID;
}
else {
var newItem = new Zotero.Item;
var sameLibrary = true;
if (includePrimary) {
newItem.id = this.id;
newItem.libraryID = this.libraryID;
newItem.key = this.key;
newItem.setType(itemTypeID);
for (var field in obj.primary) {
switch (field) {
case 'itemID':
case 'itemType':
case 'libraryID':
case 'key':
continue;
}
newItem.setField(field, obj.primary[field]);
}
}
else {
newItem.setType(itemTypeID);
}
}
var changedFields = {};
for (var field in obj.fields) {
var fieldID = Zotero.ItemFields.getID(field);
if (fieldID && Zotero.ItemFields.isValidForType(fieldID, itemTypeID)) {
newItem.setField(field, obj.fields[field]);
changedFields[field] = true;
}
}
// If modifying an existing item, clear other fields not in the cloned item
if (newItem) {
var previousFields = this.getUsedFields(true);
for each(var field in previousFields) {
if (!changedFields[field] && Zotero.ItemFields.isValidForType(field, itemTypeID)) {
newItem.setField(field, false);
}
}
} }
// Regular item // Regular item
if (this.isRegularItem()) { if (this.isRegularItem()) {
if (includePrimary) { newItem.setCreators(newItem.getCreators());
// newItem = loaded from db
// obj = in-memory
var max = Math.max(newItem.numCreators(), this.numCreators());
var deleteOffset = 0;
for (var i=0; i<max; i++) {
var newIndex = i - deleteOffset;
// Remove existing creators (loaded because we set the itemID
// above) not in the in-memory version
if (!obj.creators[i]) {
if (newItem.getCreator(newIndex)) {
newItem.removeCreator(newIndex);
deleteOffset++;
}
continue;
}
// Add in-memory creators
newItem.setCreator(
newIndex, this.getCreator(i).ref, obj.creators[i].creatorTypeID
);
}
}
else {
// If overwriting an existing item, clear existing creators
if (newItem) {
for (var i=newItem.numCreators()-1; i>=0; i--) {
if (newItem.getCreator(i)) {
newItem.removeCreator(i);
}
}
}
var i = 0;
for (var c in obj.creators) {
var creator = this.getCreator(c).ref;
var creatorTypeID = this.getCreator(c).creatorTypeID;
if (!sameLibrary) {
var creatorDataID = Zotero.Creators.getDataID(this.getCreator(c).ref);
var creatorIDs = Zotero.Creators.getCreatorsWithData(creatorDataID, newItem.libraryID);
if (creatorIDs) {
// TODO: support multiple creators?
var creator = Zotero.Creators.get(creatorIDs[0]);
}
else {
var newCreator = new Zotero.Creator;
newCreator.libraryID = newItem.libraryID;
newCreator.setFields(creator);
var creator = newCreator;
}
var creatorTypeID = this.getCreator(c).creatorTypeID;
}
newItem.setCreator(i, creator, creatorTypeID);
i++;
}
}
} }
else { else {
newItem.setNote(this.getNote()); newItem.setNote(this.getNote());
@ -3904,28 +3801,30 @@ Zotero.Item.prototype.clone = function(includePrimary, newItem, unsaved, skipTag
} }
} }
if (!skipTags && obj.tags) { if (!skipTags) {
for each(var tag in obj.tags) { newItem.setTags(this.getTags());
if (sameLibrary) {
newItem.addTagByID(tag.primary.tagID);
}
else {
newItem.addTag(tag.fields.name, tag.fields.type);
}
}
} }
if (obj.related && sameLibrary) { if (sameLibrary) {
// DEBUG: this will add reverse-only relateds too // DEBUG: this will add reverse-only relateds too
newItem.relatedItems = obj.related; newItem.setRelations(this.getRelations());
} }
Zotero.DB.commitTransaction();
return newItem; return newItem;
} }
/**
* @return {Promise<Zotero.Item>} - A copy of the item with primary data loaded
*/
Zotero.Item.prototype.copy = Zotero.Promise.coroutine(function* () {
var newItem = new Zotero.Item;
newItem.id = this.id;
yield newItem.loadPrimaryData();
return newItem;
});;
/** /**
* Delete item from database and clear from Zotero.Items internal array * Delete item from database and clear from Zotero.Items internal array
* *

View File

@ -481,65 +481,76 @@ Zotero.Items = new function() {
this.merge = function (item, otherItems) { this.merge = function (item, otherItems) {
Zotero.DB.beginTransaction(); return Zotero.DB.executeTransaction(function* () {
var otherItemIDs = [];
var itemURI = Zotero.URI.getItemURI(item);
var otherItemIDs = []; yield item.loadTags();
var itemURI = Zotero.URI.getItemURI(item); yield item.loadRelations();
for each(var otherItem in otherItems) { for each(var otherItem in otherItems) {
// Move child items to master yield otherItem.loadChildItems();
var ids = otherItem.getAttachments(true).concat(otherItem.getNotes(true)); yield otherItem.loadCollections();
for each(var id in ids) { yield otherItem.loadTags();
var attachment = Zotero.Items.get(id); yield otherItem.loadRelations();
// TODO: Skip identical children? // Move child items to master
var ids = otherItem.getAttachments(true).concat(otherItem.getNotes(true));
for each(var id in ids) {
var attachment = yield Zotero.Items.getAsync(id);
attachment.parentID = item.id; // TODO: Skip identical children?
attachment.save();
attachment.parentID = item.id;
yield attachment.save();
}
// All other operations are additive only and do not affect the,
// old item, which will be put in the trash
// Add collections to master
var collectionIDs = otherItem.getCollections();
for each(var collectionID in collectionIDs) {
let collection = yield Zotero.Collections.getAsync(collectionID);
yield collection.addItem(item.id);
}
// Add tags to master
var tags = otherItem.getTags();
for (let j = 0; j < tags.length; j++) {
item.addTag(tags[j].tag);
}
// Related items
var relatedItems = otherItem.relatedItems;
for each(var relatedItemID in relatedItems) {
yield item.addRelatedItem(relatedItemID);
}
// Relations
yield Zotero.Relations.copyURIs(
item.libraryID,
Zotero.URI.getItemURI(otherItem),
Zotero.URI.getItemURI(item)
);
// Add relation to track merge
var otherItemURI = Zotero.URI.getItemURI(otherItem);
yield Zotero.Relations.add(
item.libraryID,
otherItemURI,
Zotero.Relations.deletedItemPredicate,
itemURI
);
// Trash other item
otherItem.deleted = true;
yield otherItem.save();
} }
// All other operations are additive only and do not affect the, yield item.save();
// old item, which will be put in the trash });
};
// Add collections to master
var collectionIDs = otherItem.getCollections();
for each(var collectionID in collectionIDs) {
var collection = Zotero.Collections.get(collectionID);
collection.addItem(item.id);
}
// Add tags to master
var tags = otherItem.getTags();
for each(var tag in tags) {
item.addTagByID(tag.id);
}
// Related items
var relatedItems = otherItem.relatedItems;
for each(var relatedItemID in relatedItems) {
item.addRelatedItem(relatedItemID);
}
// Relations
Zotero.Relations.copyURIs(
item.libraryID,
Zotero.URI.getItemURI(otherItem),
Zotero.URI.getItemURI(item)
);
// Add relation to track merge
var otherItemURI = Zotero.URI.getItemURI(otherItem);
Zotero.Relations.add(item.libraryID, otherItemURI, Zotero.Relations.deletedItemPredicate, itemURI);
// Trash other item
otherItem.deleted = true;
otherItem.save();
}
item.save();
Zotero.DB.commitTransaction();
}
this.trash = function (ids) { this.trash = function (ids) {

View File

@ -79,11 +79,14 @@ Zotero.Relations = new function () {
} }
var toReturn = []; var toReturn = [];
var loads = [];
for (let i=0; i<rows.length; i++) { for (let i=0; i<rows.length; i++) {
var relation = new Zotero.Relation; var relation = new Zotero.Relation;
relation.id = rows[i]; relation.id = rows[i];
loads.push(relation.load());
toReturn.push(relation); toReturn.push(relation);
} }
yield Zotero.Promise.all(loads);
return toReturn; return toReturn;
}); });

View File

@ -54,13 +54,15 @@ Zotero.Duplicates.prototype.getSearchObject = Zotero.Promise.coroutine(function*
+ "(id INTEGER PRIMARY KEY)"; + "(id INTEGER PRIMARY KEY)";
yield Zotero.DB.queryAsync(sql); yield Zotero.DB.queryAsync(sql);
this._findDuplicates(); yield this._findDuplicates();
var ids = this._sets.findAll(true); var ids = this._sets.findAll(true);
Zotero.debug("Inserting rows into temp table");
sql = "INSERT INTO tmpDuplicates VALUES (?)"; sql = "INSERT INTO tmpDuplicates VALUES (?)";
for (let i=0; i<ids.length; i++) { for (let i=0; i<ids.length; i++) {
yield Zotero.DB.queryAsync(sql, [ids[i]], { debug: false }) yield Zotero.DB.queryAsync(sql, [ids[i]], { debug: false })
} }
Zotero.debug("Done");
}.bind(this)); }.bind(this));
var s = new Zotero.Search; var s = new Zotero.Search;
@ -88,7 +90,7 @@ Zotero.Duplicates.prototype._getObjectFromID = function (id) {
} }
Zotero.Duplicates.prototype._findDuplicates = function () { Zotero.Duplicates.prototype._findDuplicates = Zotero.Promise.coroutine(function* () {
var start = Date.now(); var start = Date.now();
var self = this; var self = this;
@ -136,8 +138,8 @@ Zotero.Duplicates.prototype._findDuplicates = function () {
* set and the next start row would be a * set and the next start row would be a
* different title. * different title.
*/ */
function processRows(compareRows, reprocessMatches) { function processRows(rows, compareRows, reprocessMatches) {
if (!rows) { if (!rows.length) {
return; return;
} }
@ -182,7 +184,7 @@ Zotero.Duplicates.prototype._findDuplicates = function () {
+ "JOIN itemDataValues USING (valueID) " + "JOIN itemDataValues USING (valueID) "
+ "WHERE libraryID=? AND itemTypeID=? AND fieldID=? " + "WHERE libraryID=? AND itemTypeID=? AND fieldID=? "
+ "AND itemID NOT IN (SELECT itemID FROM deletedItems)"; + "AND itemID NOT IN (SELECT itemID FROM deletedItems)";
var rows = Zotero.DB.query( var rows = yield Zotero.DB.queryAsync(
sql, sql,
[ [
this._libraryID, this._libraryID,
@ -191,29 +193,43 @@ Zotero.Duplicates.prototype._findDuplicates = function () {
] ]
); );
var isbnCache = {}; var isbnCache = {};
if (rows) { if (rows.length) {
for each(var row in rows) { let newRows = [];
row.value = (row.value+'').replace(/[^\dX]+/ig, '').toUpperCase(); //ignore formatting for (let i = 0; i < rows.length; i++) {
isbnCache[row.itemID] = row.value; let row = rows[i];
// Ignore formatting
let newVal = (row.value + '').replace(/[^\dX]+/ig, '').toUpperCase();
isbnCache[row.itemID] = newVal;
newRows.push({
itemID: row.itemID,
value: newVal
});
} }
rows.sort(sortByValue); newRows.sort(sortByValue);
processRows(); processRows(newRows);
} }
// DOI // DOI
var sql = "SELECT itemID, value FROM items JOIN itemData USING (itemID) " var sql = "SELECT itemID, value FROM items JOIN itemData USING (itemID) "
+ "JOIN itemDataValues USING (valueID) " + "JOIN itemDataValues USING (valueID) "
+ "WHERE libraryID=? AND fieldID=? AND REGEXP('^10\\.', value) " + "WHERE libraryID=? AND fieldID=? AND value LIKE '10\\.%' "
+ "AND itemID NOT IN (SELECT itemID FROM deletedItems)"; + "AND itemID NOT IN (SELECT itemID FROM deletedItems)";
var rows = Zotero.DB.query(sql, [this._libraryID, Zotero.ItemFields.getID('DOI')]); var rows = yield Zotero.DB.queryAsync(sql, [this._libraryID, Zotero.ItemFields.getID('DOI')]);
var doiCache = {}; var doiCache = {};
if (rows) { if (rows.length) {
for each(var row in rows) { let newRows = [];
row.value = (row.value+'').trim().toUpperCase(); //DOIs are case insensitive for (let i = 0; i < rows.length; i++) {
doiCache[row.itemID] = row.value; let row = rows[i];
// DOIs are case insensitive
let newVal = (row.value + '').trim().toUpperCase();
doiCache[row.itemID] = newVal;
newRows.push({
itemID: row.itemID,
value: newVal
});
} }
rows.sort(sortByValue); newRows.sort(sortByValue);
processRows(); processRows(newRows);
} }
// Get years // Get years
@ -228,33 +244,70 @@ Zotero.Duplicates.prototype._findDuplicates = function () {
+ "AND SUBSTR(value, 1, 4) != '0000' " + "AND SUBSTR(value, 1, 4) != '0000' "
+ "AND itemID NOT IN (SELECT itemID FROM deletedItems) " + "AND itemID NOT IN (SELECT itemID FROM deletedItems) "
+ "ORDER BY value"; + "ORDER BY value";
var rows = Zotero.DB.query(sql, [this._libraryID].concat(dateFields)); var rows = yield Zotero.DB.queryAsync(sql, [this._libraryID].concat(dateFields));
var yearCache = {}; var yearCache = {};
if (rows) { for (let i = 0; i < rows.length; i++) {
for each(var row in rows) { let row = rows[i];
yearCache[row.itemID] = row.year; yearCache[row.itemID] = row.year;
}
} }
var creatorRowsCache = {};
// Match on normalized title // Match on normalized title
var titleIDs = Zotero.ItemFields.getTypeFieldsFromBase('title');
titleIDs.push(Zotero.ItemFields.getID('title'));
var sql = "SELECT itemID, value FROM items JOIN itemData USING (itemID) " var sql = "SELECT itemID, value FROM items JOIN itemData USING (itemID) "
+ "JOIN itemDataValues USING (valueID) " + "JOIN itemDataValues USING (valueID) "
+ "WHERE libraryID=? AND fieldID BETWEEN 110 AND 113 " + "WHERE libraryID=? AND fieldID IN "
+ "(" + titleIDs.join(', ') + ") "
+ "AND itemTypeID NOT IN (1, 14) " + "AND itemTypeID NOT IN (1, 14) "
+ "AND itemID NOT IN (SELECT itemID FROM deletedItems)"; + "AND itemID NOT IN (SELECT itemID FROM deletedItems)";
var rows = Zotero.DB.query(sql, [this._libraryID]); var rows = yield Zotero.DB.queryAsync(sql, [this._libraryID]);
if(rows) { if (rows.length) {
//normalize all values ahead of time //normalize all values ahead of time
rows = rows.map(function(row) { rows = rows.map(function(row) {
row.value = normalizeString(row.value); return {
return row; itemID: row.itemID,
value: normalizeString(row.value)
};
}); });
//sort rows by normalized values //sort rows by normalized values
rows.sort(sortByValue); rows.sort(sortByValue);
processRows(function (a, b) { // Get all creators and separate by itemID
//
// We won't need all of these, but otherwise we would have to make processRows()
// asynchronous, which would be too slow
let creatorRowsCache = {};
let sql = "SELECT itemID, lastName, firstName, fieldMode FROM items "
+ "JOIN itemCreators USING (itemID) "
+ "JOIN creators USING (creatorID) "
+ "WHERE libraryID=? AND itemTypeID NOT IN (1, 14) AND "
+ "itemID NOT IN (SELECT itemID FROM deletedItems)"
+ "ORDER BY itemID, orderIndex";
let creatorRows = yield Zotero.DB.queryAsync(sql, this._libraryID);
let lastItemID;
let itemCreators = [];
for (let i = 0; i < creatorRows.length; i++) {
let row = creatorRows[i];
if (lastItemID && row.itemID != lastItemID) {
if (itemCreators.length) {
creatorRowsCache[lastItemID] = itemCreators;
itemCreators = [];
}
}
else {
itemCreators.push({
lastName: normalizeString(row.lastName),
firstInitial: row.fieldMode == 0 ? normalizeString(row.firstName).charAt(0) : false
});
}
lastItemID = row.itemID;
}
// Add final item creators
if (itemCreators.length) {
creatorRowsCache[lastItemID] = itemCreators;
}
processRows(rows, function (a, b) {
var aTitle = a.value; var aTitle = a.value;
var bTitle = b.value; var bTitle = b.value;
@ -293,25 +346,9 @@ Zotero.Duplicates.prototype._findDuplicates = function () {
if (typeof creatorRowsCache[a.itemID] != 'undefined') { if (typeof creatorRowsCache[a.itemID] != 'undefined') {
aCreatorRows = creatorRowsCache[a.itemID]; aCreatorRows = creatorRowsCache[a.itemID];
} }
else {
var sql = "SELECT lastName, firstName, fieldMode FROM itemCreators "
+ "JOIN creators USING (creatorID) "
+ "WHERE itemID=? ORDER BY orderIndex LIMIT 10";
aCreatorRows = Zotero.DB.query(sql, a.itemID);
creatorRowsCache[a.itemID] = aCreatorRows;
}
// Check for at least one match on last name + first initial of first name
if (typeof creatorRowsCache[b.itemID] != 'undefined') { if (typeof creatorRowsCache[b.itemID] != 'undefined') {
bCreatorRows = creatorRowsCache[b.itemID]; bCreatorRows = creatorRowsCache[b.itemID];
} }
else {
var sql = "SELECT lastName, firstName, fieldMode FROM itemCreators "
+ "JOIN creators USING (creatorID) "
+ "WHERE itemID=? ORDER BY orderIndex LIMIT 10";
bCreatorRows = Zotero.DB.query(sql, b.itemID);
creatorRowsCache[b.itemID] = bCreatorRows;
}
// Match if no creators // Match if no creators
if (!aCreatorRows && !bCreatorRows) { if (!aCreatorRows && !bCreatorRows) {
@ -322,13 +359,15 @@ Zotero.Duplicates.prototype._findDuplicates = function () {
return 0; return 0;
} }
for each(var aCreatorRow in aCreatorRows) { for (let i = 0; i < aCreatorRows.length; i++) {
var aLastName = normalizeString(aCreatorRow.lastName); let aCreatorRow = aCreatorRows[i];
var aFirstInitial = aCreatorRow.fieldMode == 0 ? normalizeString(aCreatorRow.firstName).charAt(0) : false; let aLastName = aCreatorRow.lastName;
let aFirstInitial = aCreatorRow.firstInitial;
for each(var bCreatorRow in bCreatorRows) { for (let j = 0; j < bCreatorRows.length; j++) {
var bLastName = normalizeString(bCreatorRow.lastName); let bCreatorRow = bCreatorRows[j];
var bFirstInitial = bCreatorRow.fieldMode == 0 ? normalizeString(bCreatorRow.firstName).charAt(0) : false; let bLastName = bCreatorRow.lastName;
let bFirstInitial = bCreatorRow.firstInitial;
if (aLastName === bLastName && aFirstInitial === bFirstInitial) { if (aLastName === bLastName && aFirstInitial === bFirstInitial) {
return 1; return 1;
@ -348,12 +387,12 @@ Zotero.Duplicates.prototype._findDuplicates = function () {
+ "WHERE libraryID=? AND fieldID=? " + "WHERE libraryID=? AND fieldID=? "
+ "AND itemID NOT IN (SELECT itemID FROM deletedItems) " + "AND itemID NOT IN (SELECT itemID FROM deletedItems) "
+ "ORDER BY value"; + "ORDER BY value";
var rows = Zotero.DB.query(sql, [this._libraryID, Zotero.ItemFields.getID(field)]); var rows = yield Zotero.DB.queryAsync(sql, [this._libraryID, Zotero.ItemFields.getID(field)]);
processRows(); processRows(rows);
}*/ }*/
Zotero.debug("Found duplicates in " + (Date.now() - start) + " ms"); Zotero.debug("Found duplicates in " + (Date.now() - start) + " ms");
} });

View File

@ -869,7 +869,7 @@ Zotero.ItemTreeView.prototype.notify = Zotero.Promise.coroutine(function* (actio
// Mirror ZoteroPane.onTreeMouseDown behavior // Mirror ZoteroPane.onTreeMouseDown behavior
var itemID = this._rows[previousRow].ref.id; var itemID = this._rows[previousRow].ref.id;
var setItemIDs = collectionTreeRow.ref.getSetItemsByItemID(itemID); var setItemIDs = collectionTreeRow.ref.getSetItemsByItemID(itemID);
yield this.selectItems(setItemIDs); this.selectItems(setItemIDs);
} }
} }
else { else {

View File

@ -1387,7 +1387,7 @@ var ZoteroPane = new function()
this.itemsView._itemSelectedPromiseResolver.reject(e); this.itemsView._itemSelectedPromiseResolver.reject(e);
} }
throw e; throw e;
}.bind(this));; }.bind(this));
}; };