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
this._leftpane.ref = val.clone(true);
this._leftpane.ref = val.copy();
this._leftpane.original = val;
]]>
</setter>
@ -97,7 +97,7 @@
}
// 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;
}

View File

@ -130,16 +130,21 @@ var Zotero_Duplicates_Pane = new function () {
// Add master item's values to the beginning of each set of
// alternative values so that they're still available if the item box
// modifies the item
var diff = item.multiDiff(_otherItems, _ignoreFields);
if (diff) {
var itemValues = item.serialize()
for (var i in diff) {
diff[i].unshift(itemValues.fields[i]);
Zotero.spawn(function* () {
var diff = yield item.multiDiff(_otherItems, _ignoreFields);
if (diff) {
let itemValues = yield item.toJSON();
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);
}
var newAttachment = new Zotero.Item('attachment');
newAttachment.libraryID = libraryID;
// Link mode needs to be set when saving new attachment
newAttachment.attachmentLinkMode = linkMode;
var newAttachment = attachment.clone(libraryID);
if (attachment.isImportedAttachment()) {
// Attachment path isn't copied over by clone() if libraryID is different
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) {
newAttachment.setSource(parentItemID);
newAttachment.parentID = parentItemID;
}
newAttachment.save();
yield newAttachment.save();
// Copy over files if they exist
if (newAttachment.isImportedAttachment() && attachment.getFile()) {
@ -1091,7 +1084,7 @@ Zotero.Attachments = new function(){
Zotero.File.copyDirectory(dir, newDir);
}
newAttachment.addLinkedItem(attachment);
yield newAttachment.addLinkedItem(attachment);
return newAttachment.id;
});

View File

@ -1567,23 +1567,15 @@ Zotero.CollectionTreeView.prototype.drop = Zotero.Promise.coroutine(function* (r
return Zotero.Attachments.copyAttachmentToLibrary(item, targetLibraryID);
}
// Create new unsaved clone item in target library
var newItem = new Zotero.Item(item.itemTypeID);
newItem.libraryID = targetLibraryID;
// 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);
// Create new clone item in target library
var newItem = item.clone(targetLibraryID, false, !Zotero.Prefs.get('groups.copyTags'));
var newItemID = yield newItem.save();
// Record link
yield newItem.addLinkedItem(item);
var newID = id;
if (item.isNote()) {
return newID;
return newItemID;
}
// 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 notes = yield Zotero.Items.getAsync(noteIDs);
for each(var note in notes) {
var newNote = new Zotero.Item('note');
newNote.libraryID = targetLibraryID;
// DEBUG: save here because clone() doesn't currently work on unsaved tagged items
var id = yield newNote.save();
newNote = yield Zotero.Items.getAsync(id);
yield note.clone(false, newNote);
newNote.parentID = newItem.id;
yield newNote.save();
let newNote = note.clone(targetLibraryID);
newNote.parentID = newItemID;
yield newNote.save()
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} data2 Serialized copy of second object
* @param {Array} diff Empty array to put diff data in
* @param {Boolean} [includeMatches=false] Include all fields, even those
* that aren't different
* @param {Object} data1 - JSON of first object
* @param {Object} data2 - JSON of second object
* @param {Array} diff - Empty array to put diff data in
* @param {Boolean} [includeMatches=false] - Include all fields, even those
* that aren't different
*/
this.diff = function (data1, data2, diff, includeMatches) {
diff.push({}, {});
var numDiffs = 0;
var subs = ['primary', 'fields'];
var skipFields = ['collectionID', 'creatorID', 'itemID', 'searchID', 'tagID', 'libraryID', 'key'];
var skipFields = ['collectionKey', 'itemKey', 'searchKey'];
for each(var sub in subs) {
diff[0][sub] = {};
diff[1][sub] = {};
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++;
}
for (var field in data1) {
if (skipFields.indexOf(field) != -1) {
continue;
}
// DEBUG: some of this is probably redundant
for (var field in data2[sub]) {
if (skipFields.indexOf(field) != -1) {
continue;
}
if (diff[0][sub][field] != undefined) {
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++;
}
if (data1[field] === false && (data2[field] === false || data2[field] === undefined)) {
continue;
}
var changed = data1[field] !== data2[field];
if (includeMatches || changed) {
diff[0][field] = data1[field] !== false ? data1[field] : '';
diff[1][field] = (data2[field] !== false && data2[field] !== undefined)
? data2[field] : '';
}
if (changed) {
numDiffs++;
}
}
// DEBUG: some of this is probably redundant
for (var field in data2) {
if (skipFields.indexOf(field) != -1) {
continue;
}
if (diff[0][field] !== undefined) {
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) {
// Multipart date fields
@ -251,18 +251,11 @@ Zotero.Item.prototype.getField = function(field, unformatted, includeBaseMapped)
* @return {Integer{}|String[]}
*/
Zotero.Item.prototype.getUsedFields = Zotero.Promise.coroutine(function* (asNames) {
if (!this.id) {
return [];
}
var sql = "SELECT fieldID FROM itemData WHERE itemID=?";
if (asNames) {
sql = "SELECT fieldName FROM fields WHERE fieldID IN (" + sql + ")";
}
var fields = yield Zotero.DB.columnQueryAsync(sql, this._id);
if (!fields) {
return [];
}
return fields;
this._requireData('itemData');
return Object.keys(this._itemData)
.filter(id => this._itemData[id] !== false)
.map(id => asNames ? Zotero.ItemFields.getName(id) : parseInt(id));
});
@ -1581,7 +1574,8 @@ Zotero.Item.prototype.save = Zotero.Promise.coroutine(function* (options) {
for (let i=0; i<toAdd.length; i++) {
let tag = toAdd[i];
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]);
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,
* replace it with a manual one.
* Add a single tag to the item. If type is 1 and an automatic tag with the same name already
* exists, replace it with a manual one.
*
* A separate save() is required to update the database.
*
* @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 tags = this.getTags();
for (let i=0; i<tags.length; i++) {
let tag = tags[i];
if (tag.tag === name) {
if (!tag.type) {
if (tag.type == type) {
Zotero.debug("Tag '" + name + "' already exists on item " + this.libraryKey);
return false;
}
tag.type = 0;
tag.type = type;
changed = true;
break;
}
}
if (!changed) {
tags.push({
tag: name
tag: name,
type: type
});
}
this.setTags(tags);
@ -3706,24 +3704,25 @@ Zotero.Item.prototype.diff = function (item, includeMatches, ignoreFields) {
*
* Currently compares only item data, not primary fields
*/
Zotero.Item.prototype.multiDiff = function (otherItems, ignoreFields) {
var thisData = this.serialize();
Zotero.Item.prototype.multiDiff = Zotero.Promise.coroutine(function* (otherItems, ignoreFields) {
var thisData = yield this.toJSON();
var alternatives = {};
var hasDiffs = false;
for each(var otherItem in otherItems) {
var diff = [];
var otherData = otherItem.serialize();
var numDiffs = Zotero.Items.diff(thisData, otherData, diff);
for (let i = 0; i < otherItems.length; i++) {
let otherItem = otherItems[i];
let diff = [];
let otherData = yield otherItem.toJSON();
let numDiffs = Zotero.Items.diff(thisData, otherData, diff);
if (numDiffs) {
for (var field in diff[1].fields) {
for (let field in diff[1]) {
if (ignoreFields && ignoreFields.indexOf(field) != -1) {
continue;
}
var value = diff[1].fields[field];
var value = diff[1][field];
if (!alternatives[field]) {
hasDiffs = true;
@ -3742,143 +3741,41 @@ Zotero.Item.prototype.multiDiff = function (otherItems, ignoreFields) {
}
return alternatives;
}
});
/**
* Returns an unsaved copy of the item
*
* @param {Boolean} [includePrimary=false]
* @param {Zotero.Item} [newItem=null] Target item for clone (used to pass a saved
* 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')
* @param {Number} [libraryID] - libraryID of the new item, or the same as original if omitted
* @param {Boolean} [skipTags=false] - Skip tags
*/
Zotero.Item.prototype.clone = function(includePrimary, newItem, unsaved, skipTags) {
Zotero.Item.prototype.clone = function(libraryID, skipTags) {
Zotero.debug('Cloning item ' + this.id);
if (includePrimary && newItem) {
throw ("includePrimary and newItem parameters are mutually exclusive in Zotero.Item.clone()");
if (libraryID !== undefined && libraryID !== null && typeof libraryID !== 'number') {
throw new Error("libraryID must be null or an integer");
}
if (unsaved) {
skipTags = true;
this._requireData('primaryData');
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 obj = this.serialize();
var itemTypeID = this.itemTypeID;
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);
}
}
var fieldIDs = this.getUsedFields();
for (let i = 0; i < fieldIDs.length; i++) {
let fieldID = fieldIDs[i];
newItem.setField(fieldID, this.getField(fieldID));
}
// Regular item
if (this.isRegularItem()) {
if (includePrimary) {
// 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++;
}
}
newItem.setCreators(newItem.getCreators());
}
else {
newItem.setNote(this.getNote());
@ -3904,28 +3801,30 @@ Zotero.Item.prototype.clone = function(includePrimary, newItem, unsaved, skipTag
}
}
if (!skipTags && obj.tags) {
for each(var tag in obj.tags) {
if (sameLibrary) {
newItem.addTagByID(tag.primary.tagID);
}
else {
newItem.addTag(tag.fields.name, tag.fields.type);
}
}
if (!skipTags) {
newItem.setTags(this.getTags());
}
if (obj.related && sameLibrary) {
if (sameLibrary) {
// DEBUG: this will add reverse-only relateds too
newItem.relatedItems = obj.related;
newItem.setRelations(this.getRelations());
}
Zotero.DB.commitTransaction();
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
*

View File

@ -481,65 +481,76 @@ Zotero.Items = new function() {
this.merge = function (item, otherItems) {
Zotero.DB.beginTransaction();
var otherItemIDs = [];
var itemURI = Zotero.URI.getItemURI(item);
for each(var otherItem in otherItems) {
// Move child items to master
var ids = otherItem.getAttachments(true).concat(otherItem.getNotes(true));
for each(var id in ids) {
var attachment = Zotero.Items.get(id);
return Zotero.DB.executeTransaction(function* () {
var otherItemIDs = [];
var itemURI = Zotero.URI.getItemURI(item);
yield item.loadTags();
yield item.loadRelations();
for each(var otherItem in otherItems) {
yield otherItem.loadChildItems();
yield otherItem.loadCollections();
yield otherItem.loadTags();
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);
// TODO: Skip identical children?
attachment.parentID = item.id;
yield attachment.save();
}
attachment.parentID = item.id;
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,
// 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();
}
yield item.save();
});
};
this.trash = function (ids) {

View File

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

View File

@ -54,13 +54,15 @@ Zotero.Duplicates.prototype.getSearchObject = Zotero.Promise.coroutine(function*
+ "(id INTEGER PRIMARY KEY)";
yield Zotero.DB.queryAsync(sql);
this._findDuplicates();
yield this._findDuplicates();
var ids = this._sets.findAll(true);
Zotero.debug("Inserting rows into temp table");
sql = "INSERT INTO tmpDuplicates VALUES (?)";
for (let i=0; i<ids.length; i++) {
yield Zotero.DB.queryAsync(sql, [ids[i]], { debug: false })
}
Zotero.debug("Done");
}.bind(this));
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 self = this;
@ -136,8 +138,8 @@ Zotero.Duplicates.prototype._findDuplicates = function () {
* set and the next start row would be a
* different title.
*/
function processRows(compareRows, reprocessMatches) {
if (!rows) {
function processRows(rows, compareRows, reprocessMatches) {
if (!rows.length) {
return;
}
@ -182,7 +184,7 @@ Zotero.Duplicates.prototype._findDuplicates = function () {
+ "JOIN itemDataValues USING (valueID) "
+ "WHERE libraryID=? AND itemTypeID=? AND fieldID=? "
+ "AND itemID NOT IN (SELECT itemID FROM deletedItems)";
var rows = Zotero.DB.query(
var rows = yield Zotero.DB.queryAsync(
sql,
[
this._libraryID,
@ -191,29 +193,43 @@ Zotero.Duplicates.prototype._findDuplicates = function () {
]
);
var isbnCache = {};
if (rows) {
for each(var row in rows) {
row.value = (row.value+'').replace(/[^\dX]+/ig, '').toUpperCase(); //ignore formatting
isbnCache[row.itemID] = row.value;
if (rows.length) {
let newRows = [];
for (let i = 0; i < rows.length; i++) {
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);
processRows();
newRows.sort(sortByValue);
processRows(newRows);
}
// DOI
var sql = "SELECT itemID, value FROM items JOIN itemData USING (itemID) "
+ "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)";
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 = {};
if (rows) {
for each(var row in rows) {
row.value = (row.value+'').trim().toUpperCase(); //DOIs are case insensitive
doiCache[row.itemID] = row.value;
if (rows.length) {
let newRows = [];
for (let i = 0; i < rows.length; i++) {
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);
processRows();
newRows.sort(sortByValue);
processRows(newRows);
}
// Get years
@ -228,33 +244,70 @@ Zotero.Duplicates.prototype._findDuplicates = function () {
+ "AND SUBSTR(value, 1, 4) != '0000' "
+ "AND itemID NOT IN (SELECT itemID FROM deletedItems) "
+ "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 = {};
if (rows) {
for each(var row in rows) {
yearCache[row.itemID] = row.year;
}
for (let i = 0; i < rows.length; i++) {
let row = rows[i];
yearCache[row.itemID] = row.year;
}
var creatorRowsCache = {};
// 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) "
+ "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 itemID NOT IN (SELECT itemID FROM deletedItems)";
var rows = Zotero.DB.query(sql, [this._libraryID]);
if(rows) {
var rows = yield Zotero.DB.queryAsync(sql, [this._libraryID]);
if (rows.length) {
//normalize all values ahead of time
rows = rows.map(function(row) {
row.value = normalizeString(row.value);
return row;
return {
itemID: row.itemID,
value: normalizeString(row.value)
};
});
//sort rows by normalized values
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 bTitle = b.value;
@ -293,25 +346,9 @@ Zotero.Duplicates.prototype._findDuplicates = function () {
if (typeof creatorRowsCache[a.itemID] != 'undefined') {
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') {
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
if (!aCreatorRows && !bCreatorRows) {
@ -322,13 +359,15 @@ Zotero.Duplicates.prototype._findDuplicates = function () {
return 0;
}
for each(var aCreatorRow in aCreatorRows) {
var aLastName = normalizeString(aCreatorRow.lastName);
var aFirstInitial = aCreatorRow.fieldMode == 0 ? normalizeString(aCreatorRow.firstName).charAt(0) : false;
for (let i = 0; i < aCreatorRows.length; i++) {
let aCreatorRow = aCreatorRows[i];
let aLastName = aCreatorRow.lastName;
let aFirstInitial = aCreatorRow.firstInitial;
for each(var bCreatorRow in bCreatorRows) {
var bLastName = normalizeString(bCreatorRow.lastName);
var bFirstInitial = bCreatorRow.fieldMode == 0 ? normalizeString(bCreatorRow.firstName).charAt(0) : false;
for (let j = 0; j < bCreatorRows.length; j++) {
let bCreatorRow = bCreatorRows[j];
let bLastName = bCreatorRow.lastName;
let bFirstInitial = bCreatorRow.firstInitial;
if (aLastName === bLastName && aFirstInitial === bFirstInitial) {
return 1;
@ -348,12 +387,12 @@ Zotero.Duplicates.prototype._findDuplicates = function () {
+ "WHERE libraryID=? AND fieldID=? "
+ "AND itemID NOT IN (SELECT itemID FROM deletedItems) "
+ "ORDER BY value";
var rows = Zotero.DB.query(sql, [this._libraryID, Zotero.ItemFields.getID(field)]);
processRows();
var rows = yield Zotero.DB.queryAsync(sql, [this._libraryID, Zotero.ItemFields.getID(field)]);
processRows(rows);
}*/
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
var itemID = this._rows[previousRow].ref.id;
var setItemIDs = collectionTreeRow.ref.getSetItemsByItemID(itemID);
yield this.selectItems(setItemIDs);
this.selectItems(setItemIDs);
}
}
else {

View File

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