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:
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