Adds sync support for related items

Might fix (or break) other stuff, but who remembers?
This commit is contained in:
Dan Stillman 2008-06-25 00:26:55 +00:00
parent 19b08a604a
commit 33de40ad95
7 changed files with 630 additions and 296 deletions

View file

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

View file

@ -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.';

View file

@ -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();
}

View file

@ -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
*

View file

@ -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 = {};

View file

@ -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;
}
// Create locally
obj = Zotero.Sync.Server.Data['xmlTo' + Type](xmlNode);
}
// 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 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 = [];
// 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;
}
}
var id = obj.save();
// Store subcollections
if (type == 'collection') {
// 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/>;

View file

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