www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | Submodules | README | LICENSE

commit 33de40ad9527efaf712c609dc9c6a7e89acb5bb4
parent 19b08a604ac319492b0814cf7117313d4d43557a
Author: Dan Stillman <dstillman@zotero.org>
Date:   Wed, 25 Jun 2008 00:26:55 +0000

Adds sync support for related items

Might fix (or break) other stuff, but who remembers?



Diffstat:
Mchrome/content/zotero/bindings/noteeditor.xml | 8+++++---
Mchrome/content/zotero/bindings/relatedbox.xml | 72+++++++++++++++++++++++++++++++++---------------------------------------
Mchrome/content/zotero/xpcom/data/item.js | 510+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Mchrome/content/zotero/xpcom/data/items.js | 9+++++++++
Mchrome/content/zotero/xpcom/data/tag.js | 116++++++++++++++++++++++++++++++++++++++++----------------------------------------
Mchrome/content/zotero/xpcom/sync.js | 205+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Mchrome/content/zotero/xpcom/translate.js | 6++++--
7 files changed, 630 insertions(+), 296 deletions(-)

diff --git a/chrome/content/zotero/bindings/noteeditor.xml b/chrome/content/zotero/bindings/noteeditor.xml @@ -407,11 +407,13 @@ <method name="seeAlsoClick"> <body> <![CDATA[ - var seealsoList = this.item.getSeeAlso(); - if(seealsoList && seealsoList.length > 0) + var relatedList = this.item.relatedItemsBidirectional; + if (relatedList.length > 0) { this.id('seeAlsoPopup').showPopup(this.id('seeAlsoLabel'),-1,-1,'popup',0,0); - else + } + else { this.id('seeAlso').add(); + } ]]> </body> </method> diff --git a/chrome/content/zotero/bindings/relatedbox.xml b/chrome/content/zotero/bindings/relatedbox.xml @@ -42,15 +42,12 @@ <![CDATA[ var r = ""; - if(this.item) - { - var seealso = this.item.getSeeAlso(); - if(seealso) - { - seealso = Zotero.Items.get(seealso); - for(var i = 0; i < seealso.length; i++) - { - r = r + seealso[i].getField('title') + ", "; + if (this.item) { + var related = this.item.relatedItemsBidirectional; + if (related) { + related = Zotero.Items.get(related); + for(var i = 0; i < related.length; i++) { + r = r + related[i].getField('title') + ", "; } r = r.substr(0,r.length-2); } @@ -67,20 +64,16 @@ while(rows.hasChildNodes()) rows.removeChild(rows.firstChild); - if(this.item) - { - var seealso = this.item.getSeeAlso(); - - if(seealso) - { - seealso = Zotero.Items.get(seealso); - for(var i = 0; i < seealso.length; i++) - { + if (this.item) { + var related = this.item.relatedItemsBidirectional; + if (related) { + related = Zotero.Items.get(related); + for (var i = 0; i < related.length; i++) { var icon= document.createElement("image"); - var type = Zotero.ItemTypes.getName(seealso[i].getType()); + var type = Zotero.ItemTypes.getName(related[i].itemTypeID); if (type=='attachment') { - switch (seealso[i].getAttachmentLinkMode()) + switch (related[i].getAttachmentLinkMode()) { case Zotero.Attachments.LINK_MODE_LINKED_URL: type += '-web-link'; @@ -102,12 +95,13 @@ icon.setAttribute('src','chrome://zotero/skin/treeitem-' + type + '.png'); var label = document.createElement("label"); - label.setAttribute('value', seealso[i].getField('title')); + label.setAttribute('value', related[i].getField('title')); label.setAttribute('crop','end'); label.setAttribute('flex','1'); var box = document.createElement('box'); - box.setAttribute('onclick',"this.parentNode.parentNode.parentNode.parentNode.parentNode.showItem('"+seealso[i].getID()+"')"); + box.setAttribute('onclick', + "document.getBindingParent(this).showItem('" + related[i].id + "')"); box.setAttribute('class','zotero-clicky'); box.setAttribute('flex','1'); box.appendChild(icon); @@ -115,16 +109,17 @@ var remove = document.createElement("label"); remove.setAttribute('value','-'); - remove.setAttribute('onclick',"this.parentNode.parentNode.parentNode.parentNode.parentNode.remove('"+seealso[i].getID()+"');"); + remove.setAttribute('onclick', + "document.getBindingParent(this).remove('" + related[i].id + "');"); remove.setAttribute('class','zotero-clicky'); var row = document.createElement("row"); row.appendChild(box); row.appendChild(remove); - row.setAttribute('id','seealso-'+seealso[i].getID()); + row.setAttribute('id', 'seealso-' + related[i].id); rows.appendChild(row); } - this.updateCount(seealso.length); + this.updateCount(related.length); } else { @@ -146,8 +141,9 @@ { for(var i = 0; i < io.dataOut.length; i++) { - this.item.addSeeAlso(io.dataOut[i]); + this.item.addRelatedItem(io.dataOut[i]); } + this.item.save(); } ]]> </body> @@ -156,12 +152,15 @@ <parameter name="id"/> <body> <![CDATA[ - if(id) - { - this.item.removeSeeAlso(id); - var rows = this.id('seeAlsoRows'); - rows.removeChild(this.id('seealso-'+id)); - this.updateCount(); + if(id) { + // TODO: set attribute on reload to determine + // which of these is necessary + this.item.removeRelatedItem(id); + this.item.save(); + + var item = Zotero.Items.get(id); + item.removeRelatedItem(this.item.id); + item.save(); } ]]> </body> @@ -206,13 +205,8 @@ <parameter name="count"/> <body> <![CDATA[ - if(count == null) - { - var seealso = this.item.getSeeAlso(); - if(seealso) - count = seealso.length; - else - count = 0; + if (count == null) { + var count = this.item.relatedItemsBidirectional.length; } var str = 'pane.item.related.count.'; diff --git a/chrome/content/zotero/xpcom/data/item.js b/chrome/content/zotero/xpcom/data/item.js @@ -69,7 +69,9 @@ Zotero.Item.prototype._init = function () { this._primaryDataLoaded = false; this._creatorsLoaded = false; this._itemDataLoaded = false; + this._relatedItemsLoaded = false; + this._changed = false; this._changedPrimaryData = false; this._changedItemData = false; this._changedCreators = false; @@ -87,6 +89,8 @@ Zotero.Item.prototype._init = function () { this._attachmentMIMEType = null; this._attachmentCharset = null; this._attachmentPath = null; + + this._relatedItems = false; } @@ -100,6 +104,10 @@ Zotero.Item.prototype.__defineGetter__('firstCreator', function () { return this //Zotero.Item.prototype.__defineGetter__('numNotes', function () { return this._itemID; }); //Zotero.Item.prototype.__defineGetter__('numAttachments', function () { return this._itemID; }); +Zotero.Item.prototype.__defineGetter__('relatedItems', function () { var ids = this._getRelatedItems(true); return ids ? ids : []; }); +Zotero.Item.prototype.__defineSetter__('relatedItems', function (arr) { this._setRelatedItems(arr); }); +Zotero.Item.prototype.__defineGetter__('relatedItemsReverse', function () { var ids = this._getRelatedItemsReverse(); return ids ? ids : []; }); +Zotero.Item.prototype.__defineGetter__('relatedItemsBidirectional', function () { var ids = this._getRelatedItemsBidirectional(); return ids ? ids : []; }); /* * Deprecated -- use id property @@ -329,7 +337,8 @@ Zotero.Item.prototype.loadFromRow = function(row, reload) { * Check if any data fields have changed since last save */ Zotero.Item.prototype.hasChanged = function() { - return !!(this._changedPrimaryData + return !!(this._changed + || this._changedPrimaryData || this._changedCreators || this._changedItemData || this._changedNote @@ -906,6 +915,69 @@ Zotero.Item.prototype.removeCreator = function(orderIndex) { } +Zotero.Item.prototype.addRelatedItem = function (itemID) { + var parsedInt = parseInt(itemID); + if (parsedInt != itemID) { + throw ("itemID '" + itemID + "' not an integer in Zotero.Item.addRelatedItem()"); + } + itemID = parsedInt; + + if (itemID == this.id) { + Zotero.debug("Can't relate item to itself in Zotero.Item.addRelatedItem()", 2); + return false; + } + + var current = this._getRelatedItems(true); + if (current && current.indexOf(itemID) != -1) { + Zotero.debug("Item " + this.id + " already related to item " + + itemID + " in Zotero.Item.addItem()"); + return false; + } + + var item = Zotero.Items.get(itemID); + if (!item) { + throw ("Can't relate item to invalid item " + itemID + + " in Zotero.Item.addRelatedItem()"); + } + /* + var otherCurrent = item.relatedItems; + if (otherCurrent.length && otherCurrent.indexOf(this.id) != -1) { + Zotero.debug("Other item " + itemID + " already related to item " + + this.id + " in Zotero.Item.addItem()"); + return false; + } + */ + + this._prepFieldChange('relatedItems'); + this._relatedItems.push(item); + return true; +} + + +Zotero.Item.prototype.removeRelatedItem = function (itemID) { + var parsedInt = parseInt(itemID); + if (parsedInt != itemID) { + throw ("itemID '" + itemID + "' not an integer in Zotero.Item.removeRelatedItem()"); + } + itemID = parsedInt; + + var current = this._getRelatedItems(true); + if (current) { + var index = current.indexOf(itemID); + } + + if (!current || index == -1) { + Zotero.debug("Item " + this.id + " isn't related to item " + + itemID + " in Zotero.Item.removeRelatedItem()"); + return false; + } + + this._prepFieldChange('relatedItems'); + this._relatedItems.splice(index, 1); + return true; +} + + /* * Save changes back to database * @@ -1232,6 +1304,59 @@ Zotero.Item.prototype.save = function() { break; } } + + + // Related items + if (this._changed.relatedItems) { + var removed = []; + var newids = []; + var currentIDs = this._getRelatedItems(true); + if (!currentIDs) { + currentIDs = []; + } + + if (this._previousData && this._previousData.related) { + for each(var id in this._previousData.related) { + if (currentIDs.indexOf(id) == -1) { + removed.push(id); + } + } + } + for each(var id in currentIDs) { + if (this._previousData && this._previousData.related && + this._previousData.related.indexOf(id) != -1) { + continue; + } + newids.push(id); + } + + if (removed.length) { + var sql = "DELETE FROM itemSeeAlso WHERE itemID=? " + + "AND linkedItemID IN (" + + removed.map(function () '?').join() + + ")"; + Zotero.DB.query(sql, [itemID].concat(removed)); + } + + if (newids.length) { + var sql = "INSERT INTO itemSeeAlso (itemID, linkedItemID) VALUES (?,?)"; + var insertStatement = Zotero.DB.getStatement(sql); + + for each(var linkedItemID in newids) { + insertStatement.bindInt32Parameter(0, itemID); + insertStatement.bindInt32Parameter(1, linkedItemID); + + try { + insertStatement.execute(); + } + catch (e) { + throw (e + ' [ERROR: ' + Zotero.DB.getLastErrorString() + ']'); + } + } + } + + Zotero.Notifier.trigger('modify', 'item', removed.concat(newids)); + } } // @@ -1585,6 +1710,59 @@ Zotero.Item.prototype.save = function() { } } } + + + // Related items + if (this._changed.relatedItems) { + var removed = []; + var newids = []; + var currentIDs = this._getRelatedItems(true); + if (!currentIDs) { + currentIDs = []; + } + + if (this._previousData && this._previousData.related) { + for each(var id in this._previousData.related) { + if (currentIDs.indexOf(id) == -1) { + removed.push(id); + } + } + } + for each(var id in currentIDs) { + if (this._previousData && this._previousData.related && + this._previousData.related.indexOf(id) != -1) { + continue; + } + newids.push(id); + } + + if (removed.length) { + var sql = "DELETE FROM itemSeeAlso WHERE itemID=? " + + "AND linkedItemID IN (" + + removed.map(function () '?').join() + + ")"; + Zotero.DB.query(sql, [this.id].concat(removed)); + } + + if (newids.length) { + var sql = "INSERT INTO itemSeeAlso (itemID, linkedItemID) VALUES (?,?)"; + var insertStatement = Zotero.DB.getStatement(sql); + + for each(var linkedItemID in newids) { + insertStatement.bindInt32Parameter(0, this.id); + insertStatement.bindInt32Parameter(1, linkedItemID); + + try { + insertStatement.execute(); + } + catch (e) { + throw (e + ' [ERROR: ' + Zotero.DB.getLastErrorString() + ']'); + } + } + } + + Zotero.Notifier.trigger('modify', 'item', removed.concat(newids)); + } } //Zotero.History.commit(); @@ -1625,7 +1803,6 @@ Zotero.Item.prototype.save = function() { if (isNew) { var id = this.id; - Zotero.debug('DISABLING ITEM'); this._disabled = true; return id; } @@ -2568,116 +2745,6 @@ Zotero.Item.prototype.removeAllTags = function() { } -// -// Methods dealing with See Also links -// -// save() is not required for See Also functions -// -Zotero.Item.prototype.addSeeAlso = function(itemID) { - if (itemID==this.id) { - Zotero.debug('Cannot add item as See Also of itself', 2); - return false; - } - - Zotero.DB.beginTransaction(); - - var relatedItem = Zotero.Items.get(itemID); - - if (!relatedItem) { - Zotero.DB.commitTransaction(); - throw ("Cannot add invalid item " + itemID + " as See Also"); - return false; - } - - // Check both ways, using a UNION to take advantage of indexes - var sql = "SELECT (SELECT COUNT(*) FROM itemSeeAlso WHERE itemID=?1 AND " - + "linkedItemID=?2) + (SELECT COUNT(*) FROM itemSeeAlso WHERE " - + "linkedItemID=?1 AND itemID=?2)"; - if (Zotero.DB.valueQuery(sql, [this.id, itemID])) { - Zotero.DB.commitTransaction(); - Zotero.debug("Item " + itemID + " already linked", 2); - return false; - } - - var notifierData = {}; - notifierData[this.id] = { old: this.serialize() }; - notifierData[relatedItem.id] = { old: relatedItem.serialize() }; - - var sql = "INSERT INTO itemSeeAlso VALUES (?,?)"; - Zotero.DB.query(sql, [this.id, {int:itemID}]); - Zotero.DB.commitTransaction(); - Zotero.Notifier.trigger('modify', 'item', [this.id, itemID], notifierData); - return true; -} - -Zotero.Item.prototype.removeSeeAlso = function(itemID) { - if (!this.id) { - throw ('Cannot remove related item of unsaved item'); - } - - Zotero.DB.beginTransaction(); - - var relatedItem = Zotero.Items.get(itemID); - if (!relatedItem) { - Zotero.DB.commitTransaction(); - throw ("Cannot remove invalid item " + itemID + " as See Also"); - return false; - } - - var notifierData = {}; - notifierData[this.id] = { old: this.serialize() }; - notifierData[relatedItem.id] = { old: relatedItem.serialize() }; - - var sql = "DELETE FROM itemSeeAlso WHERE itemID=? AND linkedItemID=?"; - Zotero.DB.query(sql, [this.id, itemID]); - var sql = "DELETE FROM itemSeeAlso WHERE itemID=? AND linkedItemID=?"; - Zotero.DB.query(sql, [itemID, this.id]); - Zotero.DB.commitTransaction(); - Zotero.Notifier.trigger('modify', 'item', [this.id, itemID], notifierData); -} - -Zotero.Item.prototype.removeAllRelated = function() { - if (!this.id) { - throw ('Cannot remove related items of unsaved item'); - } - - Zotero.DB.beginTransaction(); - var relateds = this.getSeeAlso(); - if (!relateds) { - Zotero.DB.commitTransaction(); - return; - } - - var notifierData = {}; - notifierData[this.id] = { old: this.serialize() }; - - for each(var id in relateds) { - var item = Zotero.Items.get(id); - if (item) { - notifierData[item.id] = { old: item.serialize() }; - } - } - - Zotero.DB.query("DELETE FROM itemSeeAlso WHERE itemID=?", this.id); - Zotero.DB.query("DELETE FROM itemSeeAlso WHERE linkedItemID=?", this.id); - Zotero.DB.commitTransaction(); - - var ids = [this.id].concat(relateds); - - Zotero.Notifier.trigger('modify', 'item', ids, notifierData); -} - -Zotero.Item.prototype.getSeeAlso = function() { - if (!this.id) { - return false; - } - // Check both ways, using a UNION to take advantage of indexes - var sql ="SELECT linkedItemID FROM itemSeeAlso WHERE itemID=?1 UNION " - + "SELECT itemID FROM itemSeeAlso WHERE linkedItemID=?1"; - return Zotero.DB.columnQuery(sql, this.id); -} - - Zotero.Item.prototype.getImageSrc = function() { var itemType = Zotero.ItemTypes.getName(this.itemTypeID); if (itemType == 'attachment') { @@ -2886,10 +2953,9 @@ Zotero.Item.prototype.clone = function(includePrimary) { } } - if (obj.seeAlso) { - for each(var id in obj.seeAlso) { - newItem.addSeeAlso(id) - } + if (obj.related) { + // DEBUG: this will add reverse-only relateds too + newItem.relatedItems = obj.related; } Zotero.DB.commitTransaction(); @@ -3017,16 +3083,16 @@ Zotero.Item.prototype.erase = function(deleteChildren) { Zotero.DB.query(sql); } - // Flag See Also links for notification - var relateds = this.getSeeAlso(); + // Flag related items for notification + var relateds = this._getRelatedItemsBidirectional(); if (relateds) { for each(var id in relateds) { - var i = Zotero.Items.get(id); - if (!changedItemsNotifierData[i.id]) { - changedItemsNotifierData[i.id] = { old: i.serialize() }; + var relatedItem = Zotero.Items.get(id); + if (changedItems.indexOf(id) != -1) { + changedItemsNotifierData[id] = { old: relatedItem.serialize() }; + changedItems.push(id); } } - changedItems = changedItems.concat(relateds); } // Clear fulltext cache @@ -3191,7 +3257,7 @@ Zotero.Item.prototype.toArray = function (mode) { } } - arr.related = this.getSeeAlso(); + arr.related = this._getRelatedItemsBidirectional(); if (!arr.related) { arr.related = []; } @@ -3319,10 +3385,10 @@ Zotero.Item.prototype.serialize = function(mode) { } } - arr.related = this.getSeeAlso(); - if (!arr.related) { - arr.related = []; - } + var related = this._getRelatedItems(true); + var reverse = this._getRelatedItemsReverse(); + arr.related = related ? related : []; + arr.relatedReverse = reverse ? reverse : []; return arr; } @@ -3394,6 +3460,196 @@ Zotero.Item.prototype._loadItemData = function() { } +Zotero.Item.prototype._loadRelatedItems = function() { + if (!this.id) { + return; + } + + if (!this._primaryDataLoaded) { + this.loadPrimaryData(true); + } + + var sql = "SELECT linkedItemID FROM itemSeeAlso WHERE itemID=?"; + var ids = Zotero.DB.columnQuery(sql, this.id); + + this._relatedItems = []; + + if (ids) { + for each(var id in ids) { + this._relatedItems.push(Zotero.Items.get(id)); + } + } + + this._relatedItemsLoaded = true; +} + + +/** + * Returns related items this item point to + * + * @param bool asIDs Return as itemIDs + * @return array Array of itemIDs, or FALSE if none + */ +Zotero.Item.prototype._getRelatedItems = function (asIDs) { + if (!this._relatedItemsLoaded) { + this._loadRelatedItems(); + } + + if (this._relatedItems.length == 0) { + return false; + } + + // Return itemIDs + if (asIDs) { + var ids = []; + for each(var item in this._relatedItems) { + ids.push(item.id); + } + return ids; + } + + // Return Zotero.Item objects + var objs = []; + for each(var item in this._relatedItems) { + objs.push(item); + } + return objs; +} + + +/** + * Returns related items that point to this item + * + * @return array Array of itemIDs, or FALSE if none + */ +Zotero.Item.prototype._getRelatedItemsReverse = function () { + if (!this.id) { + return false; + } + + var sql = "SELECT itemID FROM itemSeeAlso WHERE linkedItemID=?"; + return Zotero.DB.columnQuery(sql, this.id); +} + + +/** + * Returns related items this item points to and that point to this item + * + * @return array|bool Array of itemIDs, or false if none + */ +Zotero.Item.prototype._getRelatedItemsBidirectional = function () { + var related = this._getRelatedItems(true); + var reverse = this._getRelatedItemsReverse(); + if (reverse) { + if (!related) { + return reverse; + } + + for each(var id in reverse) { + if (related.indexOf(id) == -1) { + related.push(id); + } + } + } + else if (!related) { + return false; + } + return related; +} + + +Zotero.Item.prototype._setRelatedItems = function (itemIDs) { + if (!this._relatedItemsLoaded) { + this._loadRelatedItems(); + } + + if (itemIDs.constructor.name != 'Array') { + throw ('ids must be an array in Zotero.Items._setRelatedItems()'); + } + + var currentIDs = this._getRelatedItems(true); + if (!currentIDs) { + currentIDs = []; + } + var oldIDs = []; // children being kept + var newIDs = []; // new children + + if (itemIDs.length == 0) { + if (currentIDs.length == 0) { + Zotero.debug('No related items added', 4); + return false; + } + } + else { + for (var i in itemIDs) { + var id = itemIDs[i]; + var parsedInt = parseInt(id); + if (parsedInt != id) { + throw ("itemID '" + id + "' not an integer in Zotero.Item.addRelatedItem()"); + } + id = parsedInt; + + if (id == this.id) { + Zotero.debug("Can't relate item to itself in Zotero.Item._setRelatedItems()", 2); + continue; + } + + if (currentIDs.indexOf(id) != -1) { + Zotero.debug("Item " + this.id + " is already related to item " + id); + oldIDs.push(id); + continue; + } + + var item = Zotero.Items.get(id); + if (!item) { + throw ("Can't relate item to invalid item " + id + + " in Zotero.Item._setRelatedItems()"); + } + /* + var otherCurrent = item.relatedItems; + if (otherCurrent.length && otherCurrent.indexOf(this.id) != -1) { + Zotero.debug("Other item " + id + " already related to item " + + this.id + " in Zotero.Item._setRelatedItems()"); + return false; + } + */ + + newIDs.push(id); + } + } + + // Mark as changed if new or removed ids + if (newIDs.length > 0 || oldIDs.length != currentIDs.length) { + this._prepFieldChange('relatedItems'); + } + else { + Zotero.debug('Related items not changed in Zotero.Item._setRelatedItems()', 4); + return false; + } + + newIDs = oldIDs.concat(newIDs); + this._relatedItems = []; + for each(var itemID in newIDs) { + this._relatedItems.push(Zotero.Items.get(itemID)); + } + return true; +} + + +// TODO: use for stuff other than related items +Zotero.Item.prototype._prepFieldChange = function (field) { + if (!this._changed) { + this._changed = {}; + } + this._changed[field] = true; + + // Save a copy of the data before changing + if (this.id && this.exists() && !this._previousData) { + this._previousData = this.serialize(); + } +} + + Zotero.Item.prototype._generateKey = function () { return Zotero.ID.getKey(); } diff --git a/chrome/content/zotero/xpcom/data/items.js b/chrome/content/zotero/xpcom/data/items.js @@ -27,6 +27,7 @@ Zotero.Items = new function() { // Privileged methods this.get = get; + this.exist = exist; this.getAll = getAll; this.getUpdated = getUpdated; this.add = add; @@ -100,6 +101,14 @@ Zotero.Items = new function() { } + function exist(itemIDs) { + var sql = "SELECT itemID FROM items WHERE itemID IN (" + + itemIDs.map(function () '?').join() + ")"; + var exist = Zotero.DB.columnQuery(sql, itemIDs); + return exist ? exist : []; + } + + /* * Returns all items in the database * diff --git a/chrome/content/zotero/xpcom/data/tag.js b/chrome/content/zotero/xpcom/data/tag.js @@ -167,64 +167,6 @@ Zotero.Tag.prototype.getLinkedItems = function (asIDs) { } -Zotero.Tag.prototype._setLinkedItems = function (itemIDs) { - if (!this._linkedItemsLoaded) { - this._loadLinkedItems(); - } - - if (itemIDs.constructor.name != 'Array') { - throw ('ids must be an array in Zotero.Tag._setLinkedItems()'); - } - - var currentIDs = this.getLinkedItems(true); - if (!currentIDs) { - currentIDs = []; - } - var oldIDs = []; // children being kept - var newIDs = []; // new children - - if (itemIDs.length == 0) { - if (currentIDs.length == 0) { - Zotero.debug('No linked items added', 4); - return false; - } - } - else { - for (var i in itemIDs) { - var id = parseInt(itemIDs[i]); - if (isNaN(id)) { - throw ("Invalid itemID '" + itemIDs[i] - + "' in Zotero.Tag._setLinkedItems()"); - } - - if (currentIDs.indexOf(id) != -1) { - Zotero.debug("Item " + itemIDs[i] - + " is already linked to tag " + this.id); - oldIDs.push(id); - continue; - } - - newIDs.push(id); - } - } - - // Mark as changed if new or removed ids - if (newIDs.length > 0 || oldIDs.length != currentIDs.length) { - this._prepFieldChange('linkedItems'); - } - else { - Zotero.debug('Linked items not changed in Zotero.Tag._setLinkedItems()', 4); - return false; - } - - newIDs = oldIDs.concat(newIDs); - - var items = Zotero.Items.get(itemIDs); - this._linkedItems = items ? items : []; - return true; -} - - Zotero.Tag.prototype.addItem = function (itemID) { var current = this.getLinkedItems(true); if (current && current.indexOf(itemID) != -1) { @@ -524,6 +466,64 @@ Zotero.Tag.prototype._loadLinkedItems = function() { } +Zotero.Tag.prototype._setLinkedItems = function (itemIDs) { + if (!this._linkedItemsLoaded) { + this._loadLinkedItems(); + } + + if (itemIDs.constructor.name != 'Array') { + throw ('ids must be an array in Zotero.Tag._setLinkedItems()'); + } + + var currentIDs = this.getLinkedItems(true); + if (!currentIDs) { + currentIDs = []; + } + var oldIDs = []; // children being kept + var newIDs = []; // new children + + if (itemIDs.length == 0) { + if (currentIDs.length == 0) { + Zotero.debug('No linked items added', 4); + return false; + } + } + else { + for (var i in itemIDs) { + var id = parseInt(itemIDs[i]); + if (isNaN(id)) { + throw ("Invalid itemID '" + itemIDs[i] + + "' in Zotero.Tag._setLinkedItems()"); + } + + if (currentIDs.indexOf(id) != -1) { + Zotero.debug("Item " + itemIDs[i] + + " is already linked to tag " + this.id); + oldIDs.push(id); + continue; + } + + newIDs.push(id); + } + } + + // Mark as changed if new or removed ids + if (newIDs.length > 0 || oldIDs.length != currentIDs.length) { + this._prepFieldChange('linkedItems'); + } + else { + Zotero.debug('Linked items not changed in Zotero.Tag._setLinkedItems()', 4); + return false; + } + + newIDs = oldIDs.concat(newIDs); + + var items = Zotero.Items.get(itemIDs); + this._linkedItems = items ? items : []; + return true; +} + + Zotero.Tag.prototype._prepFieldChange = function (field) { if (!this._changed) { this._changed = {}; diff --git a/chrome/content/zotero/xpcom/sync.js b/chrome/content/zotero/xpcom/sync.js @@ -1067,6 +1067,7 @@ Zotero.Sync.Server.Data = new function() { this.buildUploadXML = buildUploadXML; this.itemToXML = itemToXML; this.xmlToItem = xmlToItem; + this.removeMissingRelatedItems = removeMissingRelatedItems; this.collectionToXML = collectionToXML; this.xmlToCollection = xmlToCollection; this.creatorToXML = creatorToXML; @@ -1087,6 +1088,7 @@ Zotero.Sync.Server.Data = new function() { } var remoteCreatorStore = {}; + var relatedItemsStore = {}; Zotero.DB.beginTransaction(); @@ -1100,21 +1102,23 @@ Zotero.Sync.Server.Data = new function() { continue; } - Zotero.debug("Processing remotely changed " + types); - var toSaveParents = []; var toSaveChildren = []; var toDeleteParents = []; var toDeleteChildren = []; var toReconcile = []; + // + // Handle modified objects + // + Zotero.debug("Processing remotely changed " + types); + typeloop: for each(var xmlNode in xml[types][type]) { + var localDelete = false; + // Get local object with same id var obj = Zotero[Types].get(parseInt(xmlNode.@id)); - - // TODO: check local deleted items for possible conflict - if (obj) { // Key match -- same item if (obj.key == xmlNode.@key.toString()) { @@ -1130,6 +1134,24 @@ Zotero.Sync.Server.Data = new function() { // linked to a creator whose id changed) || uploadIDs.updated[types].indexOf(obj.id) != -1) { + // Merge and store related items, since CR doesn't + // affect related items + if (type == 'item') { + // TODO: skip conflict if only related items changed + + var related = xmlNode.related.toString(); + related = related ? related.split(' ') : []; + for each(var relID in obj.relatedItems) { + if (related.indexOf(relID) == -1) { + related.push(relID); + } + } + if (related.length) { + relatedItemsStore[obj.id] = related; + } + Zotero.Sync.Server.Data.removeMissingRelatedItems(xmlNode); + } + var remoteObj = Zotero.Sync.Server.Data['xmlTo' + Type](xmlNode); // Some types we don't bother to reconcile @@ -1138,24 +1160,31 @@ Zotero.Sync.Server.Data = new function() { Zotero.Sync.addToUpdated(uploadIDs.updated.items, obj.id); continue; } - else { - obj = Zotero.Sync.Server.Data['xmlTo' + Type](xmlNode, obj); - } + + // Overwrite local below } // Mark other types for conflict resolution else { - /* - // For now, show item conflicts even if only - // dateModified changed, since we need to handle - // creator conflicts there - if (type != 'item') { - // Skip if only dateModified changed + // Skip item if dateModified is the only modified + // field (and no linked creators changed) + if (type == 'item') { var diff = obj.diff(remoteObj, false, true); if (!diff) { - continue; + // Check if creators changed + var creatorsChanged = false; + var creators = obj.getCreators(); + creators = creators.concat(remoteObj.getCreators()); + for each(var creator in creators) { + if (remoteCreatorStore[obj.id]) { + creatorsChanged = true; + break; + } + } + if (!creatorsChanged) { + continue; + } } } - */ // Will be handled by item CR for now if (type == 'creator') { @@ -1178,17 +1207,14 @@ Zotero.Sync.Server.Data = new function() { continue; } } - // Local object hasn't been modified -- overwrite - else { - obj = Zotero.Sync.Server.Data['xmlTo' + Type](xmlNode, obj); - } + + // Overwrite local below } // Key mismatch -- different objects with same id, // so change id of local object else { var oldID = parseInt(xmlNode.@id); - var newID = Zotero.ID.get(types, true); Zotero.debug("Changing " + type + " " + oldID + " id to " + newID); @@ -1222,6 +1248,9 @@ Zotero.Sync.Server.Data = new function() { // Add items linked to creators to updated array, // since their timestamps will be set to the // transaction timestamp + // + // Note: Don't need to change collection children or + // related items, since they're stored as objects if (type == 'creator') { var linkedItems = obj.getLinkedItems(); if (linkedItems) { @@ -1229,22 +1258,18 @@ Zotero.Sync.Server.Data = new function() { } } - - // Note: Don't need to change collection children - // since they're stored as objects - uploadIDs.changed[types][oldID] = { oldID: oldID, newID: newID }; - // Process new item - obj = Zotero.Sync.Server.Data['xmlTo' + Type](xmlNode); + obj = null; } } - // Object doesn't exist + + // Object doesn't exist locally else { - // Reconcile locally deleted objects + // Check if object has been deleted locally for each(var pair in uploadIDs.deleted[types]) { if (pair.id != parseInt(xmlNode.@id) || pair.key != xmlNode.@key.toString()) { @@ -1258,24 +1283,31 @@ Zotero.Sync.Server.Data = new function() { throw ('Delete reconciliation unimplemented for ' + types); } - var remoteObj = Zotero.Sync.Server.Data['xmlTo' + Type](xmlNode); - - // TODO: order reconcile by parent/child? - - toReconcile.push([ - 'deleted', - remoteObj - ]); - - continue typeloop; + localDelete = true; + } + } + + // Temporarily remove and store related items that don't yet exist + if (type == 'item') { + var missing = Zotero.Sync.Server.Data.removeMissingRelatedItems(xmlNode); + if (missing.length) { + relatedItemsStore[xmlNode.@id] = missing; } - - // Create locally - obj = Zotero.Sync.Server.Data['xmlTo' + Type](xmlNode); } + // Create or overwrite locally + obj = Zotero.Sync.Server.Data['xmlTo' + Type](xmlNode, obj); + + if (localDelete) { + // TODO: order reconcile by parent/child? + + toReconcile.push([ + 'deleted', + obj + ]); + } // Child items have to be saved after parent items - if (type == 'item' && obj.getSource()) { + else if (type == 'item' && obj.getSource()) { toSaveChildren.push(obj); } else { @@ -1348,6 +1380,7 @@ Zotero.Sync.Server.Data = new function() { for each(var obj in io.dataOut) { // TODO: do we need to make sure item isn't already being saved? + // Handle items deleted during merge if (obj.ref == 'deleted') { // Deleted item was remote if (obj.left != 'deleted') { @@ -1358,6 +1391,10 @@ Zotero.Sync.Server.Data = new function() { toDeleteChildren.push(obj.id); } + if (relatedItemsStore[obj.id]) { + delete relatedItemsStore[obj.id]; + } + uploadIDs.deleted[types].push({ id: obj.id, key: obj.left.key @@ -1394,11 +1431,9 @@ Zotero.Sync.Server.Data = new function() { } } - // Sort collections in order of parent collections, - // so referenced parent collections always exist when saving if (type == 'collection') { - var collections = []; - + // Sort collections in order of parent collections, + // so referenced parent collections always exist when saving var cmp = function (a, b) { var pA = a.parent; var pB = b.parent; @@ -1408,35 +1443,45 @@ Zotero.Sync.Server.Data = new function() { return (pA < pB) ? -1 : 1; }; toSaveParents.sort(cmp); - } - - Zotero.debug('Saving merged ' + types); - for each(var obj in toSaveParents) { - // If collection, temporarily clear subcollections before - // saving since referenced collections may not exist yet - if (type == 'collection') { - var childCollections = obj.getChildCollections(true); - if (childCollections) { - obj.childCollections = []; - } - } - - var id = obj.save(); - // Store subcollections - if (type == 'collection') { + // Temporarily remove and store subcollections before saving + // since referenced collections may not exist yet + var collections = []; + for each(var obj in toSaveParents) { + var colIDs = obj.getChildCollections(true); + if (!colIDs.length) { + continue; + } + // TODO: use exist(), like related items above + obj.childCollections = []; collections.push({ obj: obj, - childCollections: childCollections + childCollections: colIDs }); } } + + // Save objects + Zotero.debug('Saving merged ' + types); + for each(var obj in toSaveParents) { + obj.save(); + } for each(var obj in toSaveChildren) { obj.save(); } - // Set subcollections - if (type == 'collection') { + // Add back related items (which now exist) + if (type == 'item') { + for (var itemID in relatedItemsStore) { + item = Zotero.Items.get(itemID); + for each(var id in relatedItemsStore[itemID]) { + item.addRelatedItem(id); + } + item.save(); + } + } + // Add back subcollections + else if (type == 'collection') { for each(var collection in collections) { if (collection.collections) { collection.obj.childCollections = collection.collections; @@ -1631,6 +1676,11 @@ Zotero.Sync.Server.Data = new function() { xml.creator += newCreator; } + // Related items + if (item.related.length) { + xml.related = item.related.join(' '); + } + return xml; } @@ -1645,7 +1695,7 @@ Zotero.Sync.Server.Data = new function() { function xmlToItem(xmlItem, item, skipPrimary) { if (!item) { if (skipPrimary) { - item = new Zotero.Item(null); + item = new Zotero.Item; } else { item = new Zotero.Item(parseInt(xmlItem.@id)); @@ -1740,10 +1790,31 @@ Zotero.Sync.Server.Data = new function() { } } + // Related items + var related = xmlItem.related.toString(); + item.relatedItems = related ? related.split(' ') : []; + return item; } + function removeMissingRelatedItems(xmlNode) { + var missing = []; + var related = xmlNode.related.toString(); + var relIDs = related ? related.split(' ') : []; + if (relIDs.length) { + var exist = Zotero.Items.exist(relIDs); + for each(var id in relIDs) { + if (exist.indexOf(id) == -1) { + missing.push(id); + } + } + xmlNode.related = exist.join(' '); + } + return missing; + } + + function collectionToXML(collection) { var xml = <collection/>; diff --git a/chrome/content/zotero/xpcom/translate.js b/chrome/content/zotero/xpcom/translate.js @@ -1017,9 +1017,10 @@ Zotero.Translate.prototype._itemTagsAndSeeAlso = function(item, newItem) { if(item.seeAlso) { for each(var seeAlso in item.seeAlso) { if(this._IDMap[seeAlso]) { - newItem.addSeeAlso(this._IDMap[seeAlso]); + newItem.addRelatedItem(this._IDMap[seeAlso]); } } + newItem.save(); } if(item.tags) { var tagsToAdd = {}; @@ -1407,9 +1408,10 @@ Zotero.Translate.prototype._itemDone = function(item, attachedTo) { if(item.seeAlso) { for each(var seeAlso in item.seeAlso) { if(this._IDMap[seeAlso]) { - newItem.addSeeAlso(this._IDMap[seeAlso]); + newItem.addRelatedItem(this._IDMap[seeAlso]); } } + newItem.save(); } // handle tags, if this is an import translation or automatic tagging is