Relations overhaul (requires new DB upgrade from 4.0)
Relations are now properties of collections and items rather than first-class objects, stored in separate collectionRelations and itemRelations tables with ids for subjects, with foreign keys to the associated data objects. Related items now use dc:relation relations rather than a separate table (among other reasons, because API syncing won't necessarily sync both items at the same time, so they can't be stored by id). The UI assigns related-item relations bidirectionally, and checks for related-item and linked-object relations are done unidirectionally by default. dc:isReplacedBy is now dc:replaces, so that the subject is an existing object, and the predicate is now named Zotero.Attachments.replacedItemPredicate. Some additional work is still needed, notably around following replaced-item relations, and migration needs to be tested more fully, but this seems to mostly work.
This commit is contained in:
parent
75bcfcb685
commit
a740658452
24 changed files with 1414 additions and 812 deletions
|
@ -390,15 +390,15 @@
|
|||
|
||||
this.id('tags').item = this.item;
|
||||
this.updateTagsSummary();
|
||||
this.id('seeAlso').item = this.item;
|
||||
this.updateSeeAlsoSummary();
|
||||
this.id('related').item = this.item;
|
||||
this.updateRelatedSummary();
|
||||
]]>
|
||||
</setter>
|
||||
</property>
|
||||
<property name="mode">
|
||||
<setter>
|
||||
<![CDATA[
|
||||
this.id('seeAlso').mode = val;
|
||||
this.id('related').mode = val;
|
||||
this.id('tags').mode = val;
|
||||
]]>
|
||||
</setter>
|
||||
|
@ -447,7 +447,7 @@
|
|||
}, this);
|
||||
]]></body>
|
||||
</method>
|
||||
<method name="seeAlsoClick">
|
||||
<method name="relatedClick">
|
||||
<body><![CDATA[
|
||||
Zotero.spawn(function* () {
|
||||
yield this.item.loadRelations();
|
||||
|
@ -455,26 +455,26 @@
|
|||
if (relatedList.length > 0) {
|
||||
var x = this.boxObject.screenX;
|
||||
var y = this.boxObject.screenY;
|
||||
this.id('seeAlsoPopup').openPopupAtScreen(x, y, false);
|
||||
this.id('relatedPopup').openPopupAtScreen(x, y, false);
|
||||
}
|
||||
else {
|
||||
this.id('seeAlso').add();
|
||||
this.id('related').add();
|
||||
}
|
||||
}, this);
|
||||
]]></body>
|
||||
</method>
|
||||
<method name="updateSeeAlsoSummary">
|
||||
<method name="updateRelatedSummary">
|
||||
<body><![CDATA[
|
||||
Zotero.spawn(function* () {
|
||||
var v = yield this.id('seeAlso').summary;
|
||||
var v = yield this.id('related').summary;
|
||||
|
||||
if (!v || v == "") {
|
||||
v = "[" + Zotero.getString('pane.item.noteEditor.clickHere') + "]";
|
||||
}
|
||||
|
||||
this.id('seeAlsoLabel').value = Zotero.getString('itemFields.related')
|
||||
this.id('relatedLabel').value = Zotero.getString('itemFields.related')
|
||||
+ Zotero.getString('punctuation.colon');
|
||||
this.id('seeAlsoClick').value = v;
|
||||
this.id('relatedClick').value = v;
|
||||
}, this)
|
||||
]]></body>
|
||||
</method>
|
||||
|
@ -535,8 +535,8 @@
|
|||
<xul:label id="parentText" class="zotero-clicky" crop="end" onclick="document.getBindingParent(this).parentClick();"/>
|
||||
</xul:row>
|
||||
<xul:row>
|
||||
<xul:label id="seeAlsoLabel"/>
|
||||
<xul:label id="seeAlsoClick" class="zotero-clicky" crop="end" onclick="document.getBindingParent(this).seeAlsoClick();"/>
|
||||
<xul:label id="relatedLabel"/>
|
||||
<xul:label id="relatedClick" class="zotero-clicky" crop="end" onclick="document.getBindingParent(this).relatedClick();"/>
|
||||
</xul:row>
|
||||
<xul:row>
|
||||
<xul:label id="tagsLabel"/>
|
||||
|
@ -545,8 +545,8 @@
|
|||
</xul:rows>
|
||||
</xul:grid>
|
||||
<xul:popupset>
|
||||
<xul:menupopup id="seeAlsoPopup" width="300" onpopupshowing="this.firstChild.reload();">
|
||||
<xul:seealsobox id="seeAlso" flex="1"/>
|
||||
<xul:menupopup id="relatedPopup" width="300" onpopupshowing="this.firstChild.reload();">
|
||||
<xul:relatedbox id="related" flex="1"/>
|
||||
</xul:menupopup>
|
||||
<!-- The onpopup* stuff is an ugly hack to keep track of when the
|
||||
popup is open (and not the descendent autocomplete popup, which also
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
<bindings xmlns="http://www.mozilla.org/xbl"
|
||||
xmlns:xbl="http://www.mozilla.org/xbl"
|
||||
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
|
||||
<binding id="seealso-box">
|
||||
<binding id="related-box">
|
||||
<implementation>
|
||||
<!-- Modes are predefined settings groups for particular tasks -->
|
||||
<field name="_mode">"view"</field>
|
||||
|
@ -67,7 +67,7 @@
|
|||
<setter>
|
||||
<![CDATA[
|
||||
this.itemRef = val;
|
||||
this.reload();
|
||||
this.refresh();
|
||||
]]>
|
||||
</setter>
|
||||
</property>
|
||||
|
@ -80,12 +80,12 @@
|
|||
if (this.item) {
|
||||
yield this.item.loadRelations()
|
||||
.tap(() => Zotero.Promise.check(this.item));
|
||||
var related = this.item.relatedItems;
|
||||
if (related) {
|
||||
related = yield Zotero.Items.getAsync(related)
|
||||
var keys = this.item.relatedItems;
|
||||
if (keys.length) {
|
||||
let items = yield Zotero.Items.getAsync(keys)
|
||||
.tap(() => Zotero.Promise.check(this.item));
|
||||
for(var i = 0; i < related.length; i++) {
|
||||
r = r + related[i].getDisplayTitle() + ", ";
|
||||
for (let item of items) {
|
||||
r = r + item.getDisplayTitle() + ", ";
|
||||
}
|
||||
r = r.substr(0,r.length-2);
|
||||
}
|
||||
|
@ -96,87 +96,117 @@
|
|||
]]>
|
||||
</getter>
|
||||
</property>
|
||||
<method name="reload">
|
||||
|
||||
<constructor>
|
||||
<![CDATA[
|
||||
this._notifierID = Zotero.Notifier.registerObserver(this, ['item'], 'relatedbox');
|
||||
]]>
|
||||
</constructor>
|
||||
|
||||
<destructor>
|
||||
<![CDATA[
|
||||
Zotero.Notifier.unregisterObserver(this._notifierID);
|
||||
]]>
|
||||
</destructor>
|
||||
|
||||
<method name="notify">
|
||||
<parameter name="event"/>
|
||||
<parameter name="type"/>
|
||||
<parameter name="ids"/>
|
||||
<parameter name="extraData"/>
|
||||
<body><![CDATA[
|
||||
if (event != 'modify' || !this.item || !this.item.id) return;
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
let id = ids[i];
|
||||
if (id != this.item.id) {
|
||||
continue;
|
||||
}
|
||||
this.refresh();
|
||||
break;
|
||||
}
|
||||
]]></body>
|
||||
</method>
|
||||
|
||||
<method name="refresh">
|
||||
<body>
|
||||
<![CDATA[
|
||||
return Zotero.spawn(function* () {
|
||||
var addButton = this.id('addButton');
|
||||
addButton.hidden = !this.editable;
|
||||
|
||||
var rows = this.id('seeAlsoRows');
|
||||
var rows = this.id('relatedRows');
|
||||
while(rows.hasChildNodes())
|
||||
rows.removeChild(rows.firstChild);
|
||||
|
||||
if (this.item) {
|
||||
yield this.item.loadRelations()
|
||||
.tap(() => Zotero.Promise.check(this.item));
|
||||
var related = this.item.relatedItems;
|
||||
if (related) {
|
||||
related = yield Zotero.Items.getAsync(related)
|
||||
var relatedKeys = this.item.relatedItems;
|
||||
for (var i = 0; i < relatedKeys.length; i++) {
|
||||
let key = relatedKeys[i];
|
||||
let relatedItem =
|
||||
yield Zotero.Items.getByLibraryAndKeyAsync(
|
||||
this.item.libraryID, key
|
||||
)
|
||||
.tap(() => Zotero.Promise.check(this.item));
|
||||
for (var i = 0; i < related.length; i++) {
|
||||
var icon= document.createElement("image");
|
||||
icon.className = "zotero-box-icon";
|
||||
var type = Zotero.ItemTypes.getName(related[i].itemTypeID);
|
||||
if (type=='attachment')
|
||||
{
|
||||
switch (related[i].getAttachmentLinkMode())
|
||||
{
|
||||
case Zotero.Attachments.LINK_MODE_LINKED_URL:
|
||||
type += '-web-link';
|
||||
break;
|
||||
|
||||
case Zotero.Attachments.LINK_MODE_IMPORTED_URL:
|
||||
type += '-snapshot';
|
||||
break;
|
||||
|
||||
case Zotero.Attachments.LINK_MODE_LINKED_FILE:
|
||||
type += '-link';
|
||||
break;
|
||||
|
||||
case Zotero.Attachments.LINK_MODE_IMPORTED_FILE:
|
||||
type += '-file';
|
||||
break;
|
||||
}
|
||||
let id = relatedItem.id;
|
||||
yield relatedItem.loadItemData()
|
||||
.tap(() => Zotero.Promise.check(this.item));
|
||||
let icon = document.createElement("image");
|
||||
icon.className = "zotero-box-icon";
|
||||
let type = Zotero.ItemTypes.getName(relatedItem.itemTypeID);
|
||||
if (type=='attachment')
|
||||
{
|
||||
switch (relatedItem.attaachmentLinkMode) {
|
||||
case Zotero.Attachments.LINK_MODE_LINKED_URL:
|
||||
type += '-web-link';
|
||||
break;
|
||||
|
||||
case Zotero.Attachments.LINK_MODE_IMPORTED_URL:
|
||||
type += '-snapshot';
|
||||
break;
|
||||
|
||||
case Zotero.Attachments.LINK_MODE_LINKED_FILE:
|
||||
type += '-link';
|
||||
break;
|
||||
|
||||
case Zotero.Attachments.LINK_MODE_IMPORTED_FILE:
|
||||
type += '-file';
|
||||
break;
|
||||
}
|
||||
icon.setAttribute('src','chrome://zotero/skin/treeitem-' + type + '.png');
|
||||
|
||||
var label = document.createElement("label");
|
||||
label.className = "zotero-box-label";
|
||||
label.setAttribute('value', related[i].getDisplayTitle());
|
||||
label.setAttribute('crop','end');
|
||||
label.setAttribute('flex','1');
|
||||
|
||||
var box = document.createElement('box');
|
||||
box.setAttribute('onclick',
|
||||
"document.getBindingParent(this).showItem('" + related[i].id + "')");
|
||||
box.setAttribute('class','zotero-clicky');
|
||||
box.setAttribute('flex','1');
|
||||
box.appendChild(icon);
|
||||
box.appendChild(label);
|
||||
|
||||
if (this.editable) {
|
||||
var remove = document.createElement("label");
|
||||
remove.setAttribute('value','-');
|
||||
remove.setAttribute('onclick',
|
||||
"document.getBindingParent(this).remove('" + related[i].id + "');");
|
||||
remove.setAttribute('class','zotero-clicky zotero-clicky-minus');
|
||||
}
|
||||
|
||||
var row = document.createElement("row");
|
||||
row.appendChild(box);
|
||||
if (this.editable) {
|
||||
row.appendChild(remove);
|
||||
}
|
||||
row.setAttribute('id', 'seealso-' + related[i].id);
|
||||
rows.appendChild(row);
|
||||
}
|
||||
this.updateCount(related.length);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.updateCount();
|
||||
icon.setAttribute('src','chrome://zotero/skin/treeitem-' + type + '.png');
|
||||
|
||||
var label = document.createElement("label");
|
||||
label.className = "zotero-box-label";
|
||||
label.setAttribute('value', relatedItem.getDisplayTitle());
|
||||
label.setAttribute('crop','end');
|
||||
label.setAttribute('flex','1');
|
||||
|
||||
var box = document.createElement('box');
|
||||
box.setAttribute('onclick',
|
||||
"document.getBindingParent(this).showItem('" + id + "')");
|
||||
box.setAttribute('class','zotero-clicky');
|
||||
box.setAttribute('flex','1');
|
||||
box.appendChild(icon);
|
||||
box.appendChild(label);
|
||||
|
||||
if (this.editable) {
|
||||
var remove = document.createElement("label");
|
||||
remove.setAttribute('value','-');
|
||||
remove.setAttribute('onclick',
|
||||
"document.getBindingParent(this).remove('" + id + "');");
|
||||
remove.setAttribute('class','zotero-clicky zotero-clicky-minus');
|
||||
}
|
||||
|
||||
var row = document.createElement("row");
|
||||
row.appendChild(box);
|
||||
if (this.editable) {
|
||||
row.appendChild(remove);
|
||||
}
|
||||
rows.appendChild(row);
|
||||
}
|
||||
this.updateCount(relatedKeys.length);
|
||||
}
|
||||
}, this);
|
||||
]]>
|
||||
|
@ -190,22 +220,37 @@
|
|||
window.openDialog('chrome://zotero/content/selectItemsDialog.xul', '',
|
||||
'chrome,dialog=no,modal,centerscreen,resizable=yes', io);
|
||||
|
||||
if(io.dataOut) {
|
||||
if (io.dataOut.length) {
|
||||
var relItem = yield Zotero.Items.getAsync(io.dataOut[0]);
|
||||
if (relItem.libraryID != this.item.libraryID) {
|
||||
// FIXME
|
||||
var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
|
||||
.getService(Components.interfaces.nsIPromptService);
|
||||
ps.alert(null, "", "You cannot relate items in different libraries in this Zotero release.");
|
||||
return;
|
||||
if (!io.dataOut || !io.dataOut.length) {
|
||||
return;
|
||||
}
|
||||
var relItems = yield Zotero.Items.getAsync(io.dataOut);
|
||||
if (!relItems.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (relItems[0].libraryID != this.item.libraryID) {
|
||||
// FIXME
|
||||
var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
|
||||
.getService(Components.interfaces.nsIPromptService);
|
||||
ps.alert(null, "", "You cannot relate items in different libraries.");
|
||||
return;
|
||||
}
|
||||
yield Zotero.DB.executeTransaction(function* () {
|
||||
for (let relItem of relItems) {
|
||||
yield this.item.loadRelations();
|
||||
if (this.item.addRelatedItem(relItem)) {
|
||||
yield this.item.save({
|
||||
skipDateModifiedUpdate: true
|
||||
});
|
||||
}
|
||||
yield relItem.loadRelations();
|
||||
if (relItem.addRelatedItem(this.item)) {
|
||||
yield relItem.save({
|
||||
skipDateModifiedUpdate: true
|
||||
});
|
||||
}
|
||||
}
|
||||
for(var i = 0; i < io.dataOut.length; i++) {
|
||||
this.item.addRelatedItem(io.dataOut[i]);
|
||||
}
|
||||
yield this.item.save();
|
||||
}
|
||||
}.bind(this));
|
||||
}, this);
|
||||
]]></body>
|
||||
</method>
|
||||
|
@ -213,17 +258,23 @@
|
|||
<parameter name="id"/>
|
||||
<body><![CDATA[
|
||||
return Zotero.spawn(function* () {
|
||||
if(id) {
|
||||
// TODO: set attribute on reload to determine
|
||||
// which of these is necessary
|
||||
this.item.removeRelatedItem(id);
|
||||
yield this.item.save();
|
||||
|
||||
var item = yield Zotero.Items.getAsync(id);
|
||||
item.removeRelatedItem(this.item.id);
|
||||
yield item.save();
|
||||
var item = yield Zotero.Items.getAsync(id);
|
||||
if (item) {
|
||||
yield Zotero.DB.executeTransaction(function* () {
|
||||
if (this.item.removeRelatedItem(item)) {
|
||||
yield this.item.save({
|
||||
skipDateModifiedUpdate: true
|
||||
});
|
||||
}
|
||||
yield item.loadRelations();
|
||||
if (item.removeRelatedItem(this.item)) {
|
||||
yield item.save({
|
||||
skipDateModifiedUpdate: true
|
||||
});
|
||||
}
|
||||
}.bind(this));
|
||||
}
|
||||
});
|
||||
}, this);
|
||||
]]></body>
|
||||
</method>
|
||||
<method name="showItem">
|
||||
|
@ -282,7 +333,7 @@
|
|||
str += 'plural';
|
||||
break;
|
||||
}
|
||||
this.id('seeAlsoNum').value = Zotero.getString(str, [count]);
|
||||
this.id('relatedNum').value = Zotero.getString(str, [count]);
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
@ -298,7 +349,7 @@
|
|||
<content>
|
||||
<xul:vbox xbl:inherits="flex" class="zotero-box">
|
||||
<xul:hbox align="center">
|
||||
<xul:label id="seeAlsoNum"/>
|
||||
<xul:label id="relatedNum"/>
|
||||
<xul:button id="addButton" label="&zotero.item.add;"
|
||||
oncommand="this.parentNode.parentNode.parentNode.add();"/>
|
||||
</xul:hbox>
|
||||
|
@ -307,7 +358,7 @@
|
|||
<xul:column flex="1"/>
|
||||
<xul:column/>
|
||||
</xul:columns>
|
||||
<xul:rows id="seeAlsoRows"/>
|
||||
<xul:rows id="relatedRows"/>
|
||||
</xul:grid>
|
||||
</xul:vbox>
|
||||
</content>
|
||||
|
|
|
@ -82,7 +82,7 @@
|
|||
</tabpanel>
|
||||
|
||||
<tabpanel>
|
||||
<seealsobox id="zotero-editpane-related" flex="1"/>
|
||||
<relatedbox id="zotero-editpane-related" flex="1"/>
|
||||
</tabpanel>
|
||||
</tabpanels>
|
||||
</tabbox>
|
||||
|
|
|
@ -1507,7 +1507,7 @@ Zotero.CollectionTreeView.prototype.canDropCheckAsync = Zotero.Promise.coroutine
|
|||
|
||||
// Cross-library drag
|
||||
if (treeRow.ref.libraryID != item.libraryID) {
|
||||
let linkedItem = yield item.getLinkedItem(treeRow.ref.libraryID);
|
||||
let linkedItem = yield item.getLinkedItem(treeRow.ref.libraryID, true);
|
||||
if (linkedItem && !linkedItem.deleted) {
|
||||
// For drag to root, skip if linked item exists
|
||||
if (treeRow.isLibrary(true)) {
|
||||
|
@ -1613,18 +1613,13 @@ Zotero.CollectionTreeView.prototype.drop = Zotero.Promise.coroutine(function* (r
|
|||
var targetLibraryType = Zotero.Libraries.getType(targetLibraryID);
|
||||
|
||||
// Check if there's already a copy of this item in the library
|
||||
var linkedItem = yield item.getLinkedItem(targetLibraryID);
|
||||
var linkedItem = yield item.getLinkedItem(targetLibraryID, true);
|
||||
if (linkedItem) {
|
||||
// If linked item is in the trash, undelete it
|
||||
// If linked item is in the trash, undelete it and remove it from collections
|
||||
// (since it shouldn't be restored to previous collections)
|
||||
if (linkedItem.deleted) {
|
||||
yield linkedItems.loadCollections();
|
||||
// Remove from any existing collections, or else when it gets
|
||||
// undeleted it would reappear in those collections
|
||||
var collectionIDs = linkedItem.getCollections();
|
||||
for each(var collectionID in collectionIDs) {
|
||||
var col = yield Zotero.Collections.getAsync(collectionID);
|
||||
col.removeItem(linkedItem.id);
|
||||
}
|
||||
yield linkedItem.loadCollections();
|
||||
linkedItem.setCollections();
|
||||
linkedItem.deleted = false;
|
||||
yield linkedItem.save();
|
||||
}
|
||||
|
|
|
@ -560,3 +560,16 @@ Zotero.CharacterSets = new function() {
|
|||
};
|
||||
}
|
||||
|
||||
|
||||
Zotero.RelationPredicates = new function () {
|
||||
Zotero.CachedTypes.apply(this, arguments);
|
||||
this.constructor.prototype = new Zotero.CachedTypes();
|
||||
|
||||
this._typeDesc = 'relation predicate';
|
||||
this._typeDescPlural = 'relation predicates';
|
||||
this._idCol = 'predicateID';
|
||||
this._nameCol = 'predicate';
|
||||
this._table = 'relationPredicates';
|
||||
this._ignoreCase = false;
|
||||
this._allowAdd = true;
|
||||
}
|
||||
|
|
|
@ -40,7 +40,8 @@ Zotero.extendClass(Zotero.DataObject, Zotero.Collection);
|
|||
Zotero.Collection.prototype._objectType = 'collection';
|
||||
Zotero.Collection.prototype._dataTypes = Zotero.Collection._super.prototype._dataTypes.concat([
|
||||
'childCollections',
|
||||
'childItems'
|
||||
'childItems',
|
||||
'relations'
|
||||
]);
|
||||
|
||||
Zotero.defineProperty(Zotero.Collection.prototype, 'ChildObjects', {
|
||||
|
@ -388,7 +389,7 @@ Zotero.Collection.prototype.addItems = Zotero.Promise.coroutine(function* (itemI
|
|||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
Zotero.Collection.prototype.removeItem = function (itemIDs) {
|
||||
Zotero.Collection.prototype.removeItem = function (itemID) {
|
||||
return this.removeItems([itemID]);
|
||||
}
|
||||
|
||||
|
@ -591,10 +592,6 @@ Zotero.Collection.prototype._eraseData = Zotero.Promise.coroutine(function* (env
|
|||
yield this.ChildObjects.trash(del);
|
||||
}
|
||||
|
||||
// Remove relations
|
||||
var uri = Zotero.URI.getCollectionURI(this);
|
||||
yield Zotero.Relations.eraseByURI(uri);
|
||||
|
||||
var placeholders = collections.map(function () '?').join();
|
||||
|
||||
// Remove item associations for all descendent collections
|
||||
|
@ -785,30 +782,21 @@ Zotero.Collection.prototype.getDescendents = function (nested, type, includeDele
|
|||
/**
|
||||
* Return a collection in the specified library equivalent to this collection
|
||||
*/
|
||||
Zotero.Collection.prototype.getLinkedCollection = function (libraryID) {
|
||||
return this._getLinkedObject(libraryID);
|
||||
};
|
||||
Zotero.Collection.prototype.getLinkedCollection = function (libraryID, bidrectional) {
|
||||
return this._getLinkedObject(libraryID, bidrectional);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add a linked-object relation pointing to the given collection
|
||||
*
|
||||
* Does not require a separate save()
|
||||
*/
|
||||
Zotero.Collection.prototype.addLinkedCollection = Zotero.Promise.coroutine(function* (collection) {
|
||||
var url1 = Zotero.URI.getCollectionURI(this);
|
||||
var url2 = Zotero.URI.getCollectionURI(collection);
|
||||
var predicate = Zotero.Relations.linkedObjectPredicate;
|
||||
if ((yield Zotero.Relations.getByURIs(url1, predicate, url2)).length
|
||||
|| (yield Zotero.Relations.getByURIs(url2, predicate, url1)).length) {
|
||||
Zotero.debug(this._ObjectTypePlural + " " + this.key + " and " + collection.key + " are already linked");
|
||||
return false;
|
||||
}
|
||||
|
||||
// If both group libraries, store relation with source group.
|
||||
// Otherwise, store with personal library.
|
||||
var userLibraryID = Zotero.Libraries.userLibraryID;
|
||||
var libraryID = (this.libraryID != userLibraryID && collection.libraryID != userLibraryID)
|
||||
? this.libraryID
|
||||
: Zotero.Libraries.userLibraryID;
|
||||
|
||||
yield Zotero.Relations.add(libraryID, url1, predicate, url2);
|
||||
return this._addLinkedObject(collection);
|
||||
});
|
||||
|
||||
|
||||
//
|
||||
// Private methods
|
||||
//
|
||||
|
|
|
@ -50,6 +50,8 @@ Zotero.DataObject = function () {
|
|||
this._parentID = null;
|
||||
this._parentKey = null;
|
||||
|
||||
this._relations = [];
|
||||
|
||||
// Set in dataObjects.js
|
||||
this._inCache = false;
|
||||
|
||||
|
@ -266,39 +268,120 @@ Zotero.DataObject.prototype._setParentKey = function(key) {
|
|||
return true;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Relations
|
||||
//
|
||||
/**
|
||||
* Returns all relations of the object
|
||||
*
|
||||
* @return {object} Object with predicates as keys and URI[], or URI (as string)
|
||||
* in the case of a single object, as values
|
||||
* @return {Object} - Object with predicates as keys and arrays of URIs as values
|
||||
*/
|
||||
Zotero.DataObject.prototype.getRelations = function () {
|
||||
this._requireData('relations');
|
||||
|
||||
var relations = {};
|
||||
for (let i=0; i<this._relations.length; i++) {
|
||||
let relation = this._relations[i];
|
||||
|
||||
let rel = this._relations[i];
|
||||
// Relations are stored internally as predicate-object pairs
|
||||
let predicate = relation[0];
|
||||
if (relations[predicate]) {
|
||||
// If object with predicate exists, convert to an array
|
||||
if (typeof relations[predicate] == 'string') {
|
||||
relations[predicate] = [relations[predicate]];
|
||||
}
|
||||
// Add new object to array
|
||||
relations[predicate].push(relation[1]);
|
||||
}
|
||||
// Add first object as a string
|
||||
else {
|
||||
relations[predicate] = relation[1];
|
||||
let p = rel[0];
|
||||
if (!relations[p]) {
|
||||
relations[p] = [];
|
||||
}
|
||||
relations[p].push(rel[1]);
|
||||
}
|
||||
return relations;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns all relations of the object with a given predicate
|
||||
*
|
||||
* @return {String[]} - URIs linked to this object with the given predicate
|
||||
*/
|
||||
Zotero.DataObject.prototype.getRelationsByPredicate = function (predicate) {
|
||||
this._requireData('relations');
|
||||
|
||||
if (!predicate) {
|
||||
throw new Error("Predicate not provided");
|
||||
}
|
||||
|
||||
var relations = [];
|
||||
for (let i=0; i<this._relations.length; i++) {
|
||||
let rel = this._relations[i];
|
||||
// Relations are stored internally as predicate-object pairs
|
||||
let p = rel[0];
|
||||
if (p !== predicate) {
|
||||
continue;
|
||||
}
|
||||
relations.push(rel[1]);
|
||||
}
|
||||
return relations;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return {Boolean} - True if the relation has been queued, false if it already exists
|
||||
*/
|
||||
Zotero.DataObject.prototype.addRelation = function (predicate, object) {
|
||||
this._requireData('relations');
|
||||
|
||||
if (!predicate) {
|
||||
throw new Error("Predicate not provided");
|
||||
}
|
||||
if (!object) {
|
||||
throw new Error("Object not provided");
|
||||
}
|
||||
|
||||
for (let i = 0; i < this._relations.length; i++) {
|
||||
let rel = this._relations[i];
|
||||
if (rel[0] == predicate && rel[1] == object) {
|
||||
Zotero.debug("Relation " + predicate + " - " + object + " already exists for "
|
||||
+ this._objectType + " " + this.libraryKey);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
this._markFieldChange('relations', this._relations);
|
||||
this._changed.relations = true;
|
||||
this._relations.push([predicate, object]);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Zotero.DataObject.prototype.hasRelation = function (predicate, object) {
|
||||
this._requireData('relations');
|
||||
|
||||
for (let i = 0; i < this._relations.length; i++) {
|
||||
let rel = this._relations[i];
|
||||
if (rel[0] == predicate && rel[1] == object) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
Zotero.DataObject.prototype.removeRelation = function (predicate, object) {
|
||||
this._requireData('relations');
|
||||
|
||||
for (let i = 0; i < this._relations.length; i++) {
|
||||
let rel = this._relations[i];
|
||||
if (rel[0] == predicate && rel[1] == object) {
|
||||
Zotero.debug("Removing relation " + predicate + " - " + object + " from "
|
||||
+ this._objectType + " " + this.libraryKey);
|
||||
this._markFieldChange('relations', this._relations);
|
||||
this._changed.relations = true;
|
||||
this._relations.splice(i, 1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Zotero.debug("Relation " + predicate + " - " + object + " did not exist for "
|
||||
+ this._objectType + " " + this.libraryKey);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Updates the object's relations
|
||||
*
|
||||
|
@ -308,37 +391,30 @@ Zotero.DataObject.prototype.getRelations = function () {
|
|||
Zotero.DataObject.prototype.setRelations = function (newRelations) {
|
||||
this._requireData('relations');
|
||||
|
||||
// There can be more than one object for a given predicate, so build
|
||||
// flat arrays with individual predicate-object pairs so we can use
|
||||
// array_diff to determine what changed
|
||||
var oldRelations = this._relations;
|
||||
|
||||
var sortFunc = function (a, b) {
|
||||
if (a[0] < b[0]) return -1;
|
||||
if (a[0] > b[0]) return 1;
|
||||
if (a[1] < b[1]) return -1;
|
||||
if (a[1] > b[1]) return 1;
|
||||
return 0;
|
||||
};
|
||||
|
||||
var newRelationsFlat = [];
|
||||
for (let predicate in newRelations) {
|
||||
let object = newRelations[predicate];
|
||||
for (let i=0; i<object.length; i++) {
|
||||
newRelationsFlat.push([predicate, object[i]]);
|
||||
}
|
||||
}
|
||||
// Relations are stored internally as a flat array with individual predicate-object pairs,
|
||||
// so convert the incoming relations to that
|
||||
var newRelationsFlat = this._flattenRelations(newRelations);
|
||||
|
||||
var changed = false;
|
||||
if (oldRelations.length != newRelationsFlat.length) {
|
||||
changed = true;
|
||||
}
|
||||
else {
|
||||
let sortFunc = function (a, b) {
|
||||
if (a[0] < b[0]) return -1;
|
||||
if (a[0] > b[0]) return 1;
|
||||
if (a[1] < b[1]) return -1;
|
||||
if (a[1] > b[1]) return 1;
|
||||
return 0;
|
||||
};
|
||||
oldRelations.sort(sortFunc);
|
||||
newRelationsFlat.sort(sortFunc);
|
||||
|
||||
for (let i=0; i<oldRelations.length; i++) {
|
||||
if (oldRelations[i] != newRelationsFlat[i]) {
|
||||
if (oldRelations[i][0] != newRelationsFlat[i][0]
|
||||
|| oldRelations[i][1] != newRelationsFlat[i][1]) {
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
|
@ -352,7 +428,6 @@ Zotero.DataObject.prototype.setRelations = function (newRelations) {
|
|||
|
||||
this._markFieldChange('relations', this._relations);
|
||||
this._changed.relations = true;
|
||||
// Store relations internally as array of predicate-object pairs
|
||||
this._relations = newRelationsFlat;
|
||||
return true;
|
||||
}
|
||||
|
@ -360,50 +435,114 @@ Zotero.DataObject.prototype.setRelations = function (newRelations) {
|
|||
|
||||
/**
|
||||
* Return an object in the specified library equivalent to this object
|
||||
*
|
||||
* Use Zotero.Collection.getLinkedCollection() and Zotero.Item.getLinkedItem() instead of
|
||||
* calling this directly.
|
||||
*
|
||||
* @param {Integer} [libraryID]
|
||||
* @return {Object|false} Linked item, or false if not found
|
||||
* @return {Promise<Zotero.DataObject>|false} Linked object, or false if not found
|
||||
*/
|
||||
Zotero.DataObject.prototype._getLinkedObject = Zotero.Promise.coroutine(function* (libraryID) {
|
||||
Zotero.DataObject.prototype._getLinkedObject = Zotero.Promise.coroutine(function* (libraryID, bidirectional) {
|
||||
if (!libraryID) {
|
||||
throw new Error("libraryID not provided");
|
||||
}
|
||||
|
||||
if (libraryID == this._libraryID) {
|
||||
throw new Error(this._ObjectType + " is already in library " + libraryID);
|
||||
}
|
||||
|
||||
yield this.loadRelations();
|
||||
|
||||
var predicate = Zotero.Relations.linkedObjectPredicate;
|
||||
var uri = Zotero.URI['get' + this._ObjectType + 'URI'](this);
|
||||
var libraryObjectPrefix = Zotero.URI.getLibraryURI(libraryID)
|
||||
+ "/" + this._objectTypePlural + "/";
|
||||
|
||||
// Get all relations with this object as the subject or object
|
||||
var links = yield Zotero.Promise.all([
|
||||
Zotero.Relations.getSubject(false, predicate, uri),
|
||||
Zotero.Relations.getObject(uri, predicate, false)
|
||||
]);
|
||||
links = links[0].concat(links[1]);
|
||||
|
||||
if (!links.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (libraryID) {
|
||||
var libraryObjectPrefix = Zotero.URI.getLibraryURI(libraryID) + "/" + this._objectTypePlural + "/";
|
||||
}
|
||||
else {
|
||||
var libraryObjectPrefix = Zotero.URI.getCurrentUserURI() + "/" + this._objectTypePlural + "/";
|
||||
}
|
||||
|
||||
for (let i=0; i<links.length; i++) {
|
||||
let link = links[i];
|
||||
if (link.startsWith(libraryObjectPrefix)) {
|
||||
var obj = yield Zotero.URI['getURI' + this._ObjectType](link);
|
||||
// Try the relations with this as a subject
|
||||
var uris = this.getRelationsByPredicate(predicate);
|
||||
for (let i = 0; i < uris.length; i++) {
|
||||
let uri = uris[i];
|
||||
if (uri.startsWith(libraryObjectPrefix)) {
|
||||
let obj = yield Zotero.URI['getURI' + this._ObjectType](uri);
|
||||
if (!obj) {
|
||||
Zotero.debug("Referenced linked " + this._objectType + " '" + link + "' not found "
|
||||
+ "in Zotero." + this._ObjectType + ".getLinked" + this._ObjectType + "()", 2);
|
||||
Zotero.debug("Referenced linked " + this._objectType + " '" + uri + "' not found "
|
||||
+ "in Zotero." + this._ObjectType + "::getLinked" + this._ObjectType + "()", 2);
|
||||
continue;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
// Then try relations with this as an object
|
||||
if (bidirectional) {
|
||||
var thisURI = Zotero.URI['get' + this._ObjectType + 'URI'](this);
|
||||
var objects = yield Zotero.Relations.getByPredicateAndObject(
|
||||
this._objectType, predicate, thisURI
|
||||
);
|
||||
for (let i = 0; i < objects.length; i++) {
|
||||
let obj = objects[i];
|
||||
if (obj.objectType != this._objectType) {
|
||||
Zotero.logError("Found linked object of different type "
|
||||
+ "(expected " + this._objectType + ", found " + obj.objectType + ")");
|
||||
continue;
|
||||
}
|
||||
if (obj.libraryID == libraryID) {
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Add a linked-item relation to a pair of objects
|
||||
*
|
||||
* A separate save() is not required.
|
||||
*
|
||||
* @param {Zotero.DataObject} object
|
||||
* @param {Promise<Boolean>}
|
||||
*/
|
||||
Zotero.DataObject.prototype._addLinkedObject = Zotero.Promise.coroutine(function* (object) {
|
||||
if (object.libraryID == this._libraryID) {
|
||||
throw new Error("Can't add linked " + this._objectType + " in same library");
|
||||
}
|
||||
|
||||
yield this.loadRelations();
|
||||
|
||||
var predicate = Zotero.Relations.linkedObjectPredicate;
|
||||
var thisURI = Zotero.URI['get' + this._ObjectType + 'URI'](this);
|
||||
var objectURI = Zotero.URI['get' + this._ObjectType + 'URI'](object);
|
||||
|
||||
var exists = this.hasRelation(predicate, objectURI);
|
||||
if (exists) {
|
||||
Zotero.debug(this._ObjectTypePlural + " " + this.libraryKey
|
||||
+ " and " + object.libraryKey + " are already linked");
|
||||
return false;
|
||||
}
|
||||
|
||||
// If one of the items is a personal library, store relation with that. Otherwise, use
|
||||
// current item's library (which in calling code is the new, copied item, since that's what
|
||||
// the user definitely has access to).
|
||||
var userLibraryID = Zotero.Libraries.userLibraryID;
|
||||
if (this.libraryID == userLibraryID || object.libraryID != userLibraryID) {
|
||||
this.addRelation(predicate, objectURI);
|
||||
yield this.save({
|
||||
skipDateModifiedUpdate: true
|
||||
});
|
||||
}
|
||||
else {
|
||||
yield object.loadRelations();
|
||||
object.addRelation(predicate, thisURI);
|
||||
yield object.save({
|
||||
skipDateModifiedUpdate: true
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
|
||||
/*
|
||||
* Build object from database
|
||||
*/
|
||||
|
@ -462,6 +601,66 @@ Zotero.DataObject.prototype.loadPrimaryData = Zotero.Promise.coroutine(function*
|
|||
this.loadFromRow(row, reload);
|
||||
});
|
||||
|
||||
|
||||
Zotero.DataObject.prototype.loadRelations = Zotero.Promise.coroutine(function* (reload) {
|
||||
if (this._objectType != 'collection' && this._objectType != 'item') {
|
||||
throw new Error("Relations not supported for " + this._objectTypePlural);
|
||||
}
|
||||
|
||||
if (this._loaded.relations && !reload) {
|
||||
return;
|
||||
}
|
||||
|
||||
Zotero.debug("Loading relations for " + this._objectType + " " + this.libraryKey);
|
||||
|
||||
this._requireData('primaryData');
|
||||
|
||||
var sql = "SELECT predicate, object FROM " + this._objectType + "Relations "
|
||||
+ "JOIN relationPredicates USING (predicateID) "
|
||||
+ "WHERE " + this.ObjectsClass.idColumn + "=?";
|
||||
var rows = yield Zotero.DB.queryAsync(sql, this.id);
|
||||
|
||||
var relations = {};
|
||||
function addRel(predicate, object) {
|
||||
if (!relations[predicate]) {
|
||||
relations[predicate] = [];
|
||||
}
|
||||
relations[predicate].push(object);
|
||||
}
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
let row = rows[i];
|
||||
addRel(row.predicate, row.object);
|
||||
}
|
||||
|
||||
/*if (this._objectType == 'item') {
|
||||
let getURI = Zotero.URI["get" + this._ObjectType + "URI"].bind(Zotero.URI);
|
||||
let objectURI = getURI(this);
|
||||
|
||||
// Related items are bidirectional, so include any pointing to this object
|
||||
let objects = yield Zotero.Relations.getByPredicateAndObject(
|
||||
Zotero.Relations.relatedItemPredicate, objectURI
|
||||
);
|
||||
for (let i = 0; i < objects.length; i++) {
|
||||
addRel(Zotero.Relations.relatedItemPredicate, getURI(objects[i]));
|
||||
}
|
||||
|
||||
// Also include any owl:sameAs relations pointing to this object
|
||||
objects = yield Zotero.Relations.getByPredicateAndObject(
|
||||
Zotero.Relations.linkedObjectPredicate, objectURI
|
||||
);
|
||||
for (let i = 0; i < objects.length; i++) {
|
||||
addRel(Zotero.Relations.linkedObjectPredicate, getURI(objects[i]));
|
||||
}
|
||||
}*/
|
||||
|
||||
// Relations are stored as predicate-object pairs
|
||||
this._relations = this._flattenRelations(relations);
|
||||
this._loaded.relations = true;
|
||||
this._clearChanged('relations');
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Reloads loaded, changed data
|
||||
*
|
||||
|
@ -774,6 +973,53 @@ Zotero.DataObject.prototype._saveData = function (env) {
|
|||
};
|
||||
|
||||
Zotero.DataObject.prototype._finalizeSave = Zotero.Promise.coroutine(function* (env) {
|
||||
// Relations
|
||||
if (this._changed.relations) {
|
||||
let toAdd, toRemove;
|
||||
// Convert to individual JSON objects, diff, and convert back
|
||||
if (this._previousData.relations) {
|
||||
let oldRelationsJSON = this._previousData.relations.map(x => JSON.stringify(x));
|
||||
let newRelationsJSON = this._relations.map(x => JSON.stringify(x));
|
||||
toAdd = Zotero.Utilities.arrayDiff(newRelationsJSON, oldRelationsJSON)
|
||||
.map(x => JSON.parse(x));
|
||||
toRemove = Zotero.Utilities.arrayDiff(oldRelationsJSON, newRelationsJSON)
|
||||
.map(x => JSON.parse(x));
|
||||
}
|
||||
else {
|
||||
toAdd = this._relations;
|
||||
toRemove = [];
|
||||
}
|
||||
|
||||
if (toAdd.length) {
|
||||
let sql = "INSERT INTO " + this._objectType + "Relations "
|
||||
+ "(" + this._ObjectsClass.idColumn + ", predicateID, object) VALUES ";
|
||||
// Convert predicates to ids
|
||||
for (let i = 0; i < toAdd.length; i++) {
|
||||
toAdd[i][0] = yield Zotero.RelationPredicates.add(toAdd[i][0]);
|
||||
}
|
||||
yield Zotero.DB.queryAsync(
|
||||
sql + toAdd.map(x => "(?, ?, ?)").join(", "),
|
||||
toAdd.map(x => [this.id, x[0], x[1]])
|
||||
.reduce((x, y) => x.concat(y))
|
||||
);
|
||||
}
|
||||
|
||||
if (toRemove.length) {
|
||||
for (let i = 0; i < toRemove.length; i++) {
|
||||
let sql = "DELETE FROM " + this._objectType + "Relations "
|
||||
+ "WHERE " + this._ObjectsClass.idColumn + "=? AND predicateID=? AND object=?";
|
||||
yield Zotero.DB.queryAsync(
|
||||
sql,
|
||||
[
|
||||
this.id,
|
||||
(yield Zotero.RelationPredicates.add(toRemove[i][0])),
|
||||
toRemove[i][1]
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (env.isNew) {
|
||||
if (!env.skipCache) {
|
||||
// Register this object's identifiers in Zotero.DataObjects
|
||||
|
@ -937,3 +1183,32 @@ Zotero.DataObject.prototype._disabledCheck = function () {
|
|||
+ "use Zotero." + this._ObjectTypePlural + ".getAsync()");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Flatten API JSON relations object into an array of unique predicate-object pairs
|
||||
*
|
||||
* @param {Object} relations - Relations object in API JSON format, with predicates as keys
|
||||
* and arrays of URIs as objects
|
||||
* @return {Array[]} - Predicate-object pairs
|
||||
*/
|
||||
Zotero.DataObject.prototype._flattenRelations = function (relations) {
|
||||
var relationsFlat = [];
|
||||
for (let predicate in relations) {
|
||||
let object = relations[predicate];
|
||||
if (Array.isArray(object)) {
|
||||
object = Zotero.Utilities.arrayUnique(object);
|
||||
for (let i = 0; i < object.length; i++) {
|
||||
relationsFlat.push([predicate, object[i]]);
|
||||
}
|
||||
}
|
||||
else if (typeof object == 'string') {
|
||||
relationsFlat.push([predicate, object]);
|
||||
}
|
||||
else {
|
||||
Zotero.debug(object, 1);
|
||||
throw new Error("Invalid relation value");
|
||||
}
|
||||
}
|
||||
return relationsFlat;
|
||||
}
|
||||
|
|
|
@ -284,9 +284,6 @@ Zotero.Group.prototype.erase = Zotero.Promise.coroutine(function* () {
|
|||
}
|
||||
}
|
||||
|
||||
var prefix = "groups/" + this.id;
|
||||
yield Zotero.Relations.eraseByURIPrefix(Zotero.URI.defaultPrefix + prefix);
|
||||
|
||||
// Delete library row, which deletes from tags, syncDeleteLog, syncedSettings, and groups
|
||||
// tables via cascade. If any of those gain caching, they should be deleted separately.
|
||||
sql = "DELETE FROM libraries WHERE libraryID=?";
|
||||
|
|
|
@ -68,7 +68,6 @@ Zotero.Item = function(itemTypeOrID) {
|
|||
|
||||
this._tags = [];
|
||||
this._collections = [];
|
||||
this._relations = [];
|
||||
|
||||
this._bestAttachmentState = null;
|
||||
this._fileExists = null;
|
||||
|
@ -78,8 +77,6 @@ Zotero.Item = function(itemTypeOrID) {
|
|||
|
||||
this._noteAccessTime = null;
|
||||
|
||||
this._relatedItems = false;
|
||||
|
||||
if (itemTypeOrID) {
|
||||
// setType initializes type-specific properties in this._itemData
|
||||
this.setType(Zotero.ItemTypes.getID(itemTypeOrID));
|
||||
|
@ -159,8 +156,7 @@ Zotero.defineProperty(Zotero.Item.prototype, 'sortCreator', {
|
|||
get: function() this._sortCreator
|
||||
});
|
||||
Zotero.defineProperty(Zotero.Item.prototype, 'relatedItems', {
|
||||
get: function() this._getRelatedItems(true),
|
||||
set: function(arr) this._setRelatedItems(arr)
|
||||
get: function() this._getRelatedItems()
|
||||
});
|
||||
|
||||
Zotero.Item.prototype.getID = function() {
|
||||
|
@ -1021,66 +1017,37 @@ for (let name of ['deleted']) {
|
|||
}
|
||||
|
||||
|
||||
Zotero.Item.prototype.addRelatedItem = Zotero.Promise.coroutine(function* (itemID) {
|
||||
var parsedInt = parseInt(itemID);
|
||||
if (parsedInt != itemID) {
|
||||
throw ("itemID '" + itemID + "' not an integer in Zotero.Item.addRelatedItem()");
|
||||
/**
|
||||
* @param {Zotero.Item}
|
||||
* @return {Boolean}
|
||||
*/
|
||||
Zotero.Item.prototype.addRelatedItem = function (item) {
|
||||
if (!(item instanceof Zotero.Item)) {
|
||||
throw new Error("'item' must be a Zotero.Item");
|
||||
}
|
||||
itemID = parsedInt;
|
||||
|
||||
if (itemID == this.id) {
|
||||
if (item == this) {
|
||||
Zotero.debug("Can't relate item to itself in Zotero.Item.addRelatedItem()", 2);
|
||||
return false;
|
||||
}
|
||||
|
||||
var current = this._getRelatedItems(true);
|
||||
if (current.indexOf(itemID) != -1) {
|
||||
Zotero.debug("Item " + this.id + " already related to item "
|
||||
+ itemID + " in Zotero.Item.addItem()");
|
||||
return false;
|
||||
if (item.libraryID != this.libraryID) {
|
||||
throw new Error("Cannot relate item to an item in a different library");
|
||||
}
|
||||
|
||||
var item = yield this.ObjectsClass.getAsync(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._markFieldChange('related', current);
|
||||
this._changed.relatedItems = true;
|
||||
this._relatedItems.push(item);
|
||||
return true;
|
||||
});
|
||||
return this.addRelation(Zotero.Relations.relatedItemPredicate, Zotero.URI.getItemURI(item));
|
||||
}
|
||||
|
||||
|
||||
Zotero.Item.prototype.removeRelatedItem = Zotero.Promise.coroutine(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);
|
||||
var index = current.indexOf(itemID);
|
||||
|
||||
if (index == -1) {
|
||||
Zotero.debug("Item " + this.id + " isn't related to item "
|
||||
+ itemID + " in Zotero.Item.removeRelatedItem()");
|
||||
return false;
|
||||
/**
|
||||
* @param {Zotero.Item}
|
||||
*/
|
||||
Zotero.Item.prototype.removeRelatedItem = Zotero.Promise.coroutine(function* (item) {
|
||||
if (!(item instanceof Zotero.Item)) {
|
||||
throw new Error("'item' must be a Zotero.Item");
|
||||
}
|
||||
|
||||
this._markFieldChange('related', current);
|
||||
this._changed.relatedItems = true;
|
||||
this._relatedItems.splice(index, 1);
|
||||
return true;
|
||||
return this.removeRelation(Zotero.Relations.relatedItemPredicate, Zotero.URI.getItemURI(item));
|
||||
});
|
||||
|
||||
|
||||
|
@ -1385,13 +1352,16 @@ Zotero.Item.prototype._saveData = Zotero.Promise.coroutine(function* (env) {
|
|||
}
|
||||
else {
|
||||
// If undeleting, remove any merge-tracking relations
|
||||
var relations = yield Zotero.Relations.getByURIs(
|
||||
Zotero.URI.getItemURI(this),
|
||||
Zotero.Relations.deletedItemPredicate,
|
||||
false
|
||||
let predicate = Zotero.Relations.replacedItemPredicate;
|
||||
let thisURI = Zotero.URI.getItemURI(this);
|
||||
let mergeItems = yield Zotero.Relations.getByPredicateAndObject(
|
||||
'item', predicate, thisURI
|
||||
);
|
||||
for each(let relation in relations) {
|
||||
relation.erase();
|
||||
for (let mergeItem of mergeItems) {
|
||||
mergeItem.removeRelation(predicate, thisURI);
|
||||
yield mergeItem.save({
|
||||
skipDateModifiedUpdate: true
|
||||
});
|
||||
}
|
||||
|
||||
sql = "DELETE FROM deletedItems WHERE itemID=?";
|
||||
|
@ -1510,12 +1480,10 @@ Zotero.Item.prototype._saveData = Zotero.Promise.coroutine(function* (env) {
|
|||
let newTags = this._tags;
|
||||
|
||||
// Convert to individual JSON objects, diff, and convert back
|
||||
let oldTagsJSON = oldTags.map(function (x) JSON.stringify(x));
|
||||
let newTagsJSON = newTags.map(function (x) JSON.stringify(x));
|
||||
let toAdd = Zotero.Utilities.arrayDiff(newTagsJSON, oldTagsJSON)
|
||||
.map(function (x) JSON.parse(x));
|
||||
let toRemove = Zotero.Utilities.arrayDiff(oldTagsJSON, newTagsJSON)
|
||||
.map(function (x) JSON.parse(x));;
|
||||
let oldTagsJSON = oldTags.map(x => JSON.stringify(x));
|
||||
let newTagsJSON = newTags.map(x => JSON.stringify(x));
|
||||
let toAdd = Zotero.Utilities.arrayDiff(newTagsJSON, oldTagsJSON).map(x => JSON.parse(x));
|
||||
let toRemove = Zotero.Utilities.arrayDiff(oldTagsJSON, newTagsJSON).map(x => JSON.parse(x));
|
||||
|
||||
for (let i=0; i<toAdd.length; i++) {
|
||||
let tag = toAdd[i];
|
||||
|
@ -1595,72 +1563,6 @@ Zotero.Item.prototype._saveData = Zotero.Promise.coroutine(function* (env) {
|
|||
}
|
||||
}
|
||||
|
||||
// Related items
|
||||
if (this._changed.relatedItems) {
|
||||
var removed = [];
|
||||
var newids = [];
|
||||
var currentIDs = this._getRelatedItems(true);
|
||||
|
||||
for each(var id in currentIDs) {
|
||||
newids.push(id);
|
||||
}
|
||||
|
||||
if (newids.length) {
|
||||
var sql = "REPLACE INTO itemSeeAlso (itemID, linkedItemID) VALUES (?,?)";
|
||||
var replaceStatement = Zotero.DB.getAsyncStatement(sql);
|
||||
|
||||
for each(var linkedItemID in newids) {
|
||||
replaceStatement.bindInt32Parameter(0, itemID);
|
||||
replaceStatement.bindInt32Parameter(1, linkedItemID);
|
||||
|
||||
yield Zotero.DB.executeAsyncStatement(replaceStatement);
|
||||
}
|
||||
}
|
||||
|
||||
Zotero.Notifier.trigger('modify', 'item', removed.concat(newids));
|
||||
}
|
||||
|
||||
// Related items
|
||||
if (this._changed.relatedItems) {
|
||||
var removed = [];
|
||||
var newids = [];
|
||||
var currentIDs = this._getRelatedItems(true);
|
||||
|
||||
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.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()
|
||||
+ ")";
|
||||
yield Zotero.DB.queryAsync(sql, [this.id].concat(removed));
|
||||
}
|
||||
|
||||
if (newids.length) {
|
||||
var sql = "INSERT INTO itemSeeAlso (itemID, linkedItemID) VALUES (?,?)";
|
||||
var insertStatement = Zotero.DB.getAsyncStatement(sql);
|
||||
|
||||
for each(var linkedItemID in newids) {
|
||||
insertStatement.bindInt32Parameter(0, this.id);
|
||||
insertStatement.bindInt32Parameter(1, linkedItemID);
|
||||
|
||||
yield Zotero.DB.executeAsyncStatement(insertStatement);
|
||||
}
|
||||
}
|
||||
|
||||
Zotero.Notifier.queue('modify', 'item', removed.concat(newids));
|
||||
}
|
||||
|
||||
// Update child item counts and contents
|
||||
if (reloadParentChildItems) {
|
||||
for (let parentItemID in reloadParentChildItems) {
|
||||
|
@ -2411,8 +2313,8 @@ Zotero.Item.prototype._updateAttachmentStates = function (exists) {
|
|||
// standalone attachment was modified locally and remotely was changed
|
||||
// into a child attachment
|
||||
catch (e) {
|
||||
Zotero.debug("Attachment parent doesn't exist for source key "
|
||||
+ "in Zotero.Item.updateAttachmentStates()", 1);
|
||||
Zotero.logError("Attachment parent doesn't exist for source key "
|
||||
+ "in Zotero.Item.updateAttachmentStates()");
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -2421,11 +2323,15 @@ Zotero.Item.prototype._updateAttachmentStates = function (exists) {
|
|||
}
|
||||
catch (e) {
|
||||
if (e instanceof Zotero.Exception.UnloadedDataException) {
|
||||
Zotero.debug("Attachment parent not yet loaded in Zotero.Item.updateAttachmentStates()", 2);
|
||||
Zotero.logError("Attachment parent not yet loaded in Zotero.Item.updateAttachmentStates()");
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
if (!item) {
|
||||
Zotero.logError("Attachment parent doesn't exist");
|
||||
return;
|
||||
}
|
||||
item.clearBestAttachmentState();
|
||||
};
|
||||
|
||||
|
@ -3463,46 +3369,6 @@ Zotero.Item.prototype.inCollection = function (collectionID) {
|
|||
};
|
||||
|
||||
|
||||
//
|
||||
// Methods dealing with relations
|
||||
//
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Return an item in the specified library equivalent to this item
|
||||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
Zotero.Item.prototype.getLinkedItem = function (libraryID) {
|
||||
return this._getLinkedObject(libraryID);
|
||||
}
|
||||
|
||||
|
||||
Zotero.Item.prototype.addLinkedItem = Zotero.Promise.coroutine(function* (item) {
|
||||
var url1 = Zotero.URI.getItemURI(this);
|
||||
var url2 = Zotero.URI.getItemURI(item);
|
||||
var predicate = Zotero.Relations.linkedObjectPredicate;
|
||||
if ((yield Zotero.Relations.getByURIs(url1, predicate, url2)).length
|
||||
|| (yield Zotero.Relations.getByURIs(url2, predicate, url1)).length) {
|
||||
Zotero.debug("Items " + this.key + " and " + item.key + " are already linked");
|
||||
return false;
|
||||
}
|
||||
|
||||
// If one of the items is a personal library, store relation with that.
|
||||
// Otherwise, use current item's library (which in calling code is the
|
||||
// new, copied item).
|
||||
var userLibraryID = Zotero.Libraries.userLibraryID;
|
||||
var libraryID = (this.libraryID == userLibraryID || item.libraryID == userLibraryID)
|
||||
? userLibraryID
|
||||
: this.libraryID;
|
||||
|
||||
yield Zotero.Relations.add(libraryID, url1, predicate, url2);
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
Zotero.Item.prototype.getImageSrc = function() {
|
||||
var itemType = Zotero.ItemTypes.getName(this.itemTypeID);
|
||||
if (itemType == 'attachment') {
|
||||
|
@ -3902,10 +3768,6 @@ Zotero.Item.prototype._eraseData = Zotero.Promise.coroutine(function* (env) {
|
|||
|
||||
// Flag related items for notification
|
||||
// TEMP: Do something with relations
|
||||
/*var relateds = this._getRelatedItems(true);
|
||||
for each(var id in relateds) {
|
||||
let relatedItem = Zotero.Items.get(id);
|
||||
}*/
|
||||
|
||||
// Clear fulltext cache
|
||||
if (this.isAttachment()) {
|
||||
|
@ -3913,10 +3775,6 @@ Zotero.Item.prototype._eraseData = Zotero.Promise.coroutine(function* (env) {
|
|||
//Zotero.Fulltext.clearItemContent(this.id);
|
||||
}
|
||||
|
||||
// Remove relations (except for merge tracker)
|
||||
var uri = Zotero.URI.getItemURI(this);
|
||||
yield Zotero.Relations.eraseByURI(uri, [Zotero.Relations.deletedItemPredicate]);
|
||||
|
||||
env.parentItem = parentItem;
|
||||
});
|
||||
|
||||
|
@ -4149,29 +4007,7 @@ Zotero.Item.prototype.toJSON = Zotero.Promise.coroutine(function* (options) {
|
|||
|
||||
// Relations
|
||||
yield this.loadRelations();
|
||||
obj.relations = {};
|
||||
var rels = yield Zotero.Relations.getByURIs(Zotero.URI.getItemURI(this));
|
||||
for each (let rel in rels) {
|
||||
obj.relations[rel.predicate] = rel.object;
|
||||
}
|
||||
var relatedItems = this._getRelatedItems().map(function (key) {
|
||||
return this.ObjectsClass.getIDFromLibraryAndKey(this.libraryID, key);
|
||||
}.bind(this)).filter(function (val) val !== false);
|
||||
relatedItems = this.ObjectsClass.get(relatedItems);
|
||||
var pred = Zotero.Relations.relatedItemPredicate;
|
||||
for (let i=0; i<relatedItems.length; i++) {
|
||||
let item = relatedItems[i];
|
||||
let uri = Zotero.URI.getItemURI(item);
|
||||
if (obj.relations[pred]) {
|
||||
if (typeof obj.relations[pred] == 'string') {
|
||||
obj.relations[pred] = [uri];
|
||||
}
|
||||
obj.relations[pred].push(uri)
|
||||
}
|
||||
else {
|
||||
obj.relations[pred] = uri;
|
||||
}
|
||||
}
|
||||
obj.relations = this.getRelations()
|
||||
|
||||
// Deleted
|
||||
let deleted = this.deleted;
|
||||
|
@ -4589,41 +4425,25 @@ Zotero.Item.prototype.loadCollections = Zotero.Promise.coroutine(function* (relo
|
|||
});
|
||||
|
||||
|
||||
Zotero.Item.prototype.loadRelations = Zotero.Promise.coroutine(function* (reload) {
|
||||
if (this._loaded.relations && !reload) {
|
||||
return;
|
||||
}
|
||||
|
||||
Zotero.debug("Loading relations for item " + this.libraryKey);
|
||||
|
||||
this._requireData('primaryData');
|
||||
|
||||
var itemURI = Zotero.URI.getItemURI(this);
|
||||
|
||||
var relations = yield Zotero.Relations.getByURIs(itemURI);
|
||||
relations = relations.map(function (rel) [rel.predicate, rel.object]);
|
||||
|
||||
// Related items are bidirectional, so include any with this item as the object
|
||||
var reverseRelations = yield Zotero.Relations.getByURIs(
|
||||
false, Zotero.Relations.relatedItemPredicate, itemURI
|
||||
);
|
||||
for (let i=0; i<reverseRelations.length; i++) {
|
||||
let rel = reverseRelations[i];
|
||||
relations.push([rel.predicate, rel.subject]);
|
||||
}
|
||||
|
||||
// Also include any owl:sameAs relations with this item as the object
|
||||
reverseRelations = yield Zotero.Relations.getByURIs(
|
||||
false, Zotero.Relations.linkedObjectPredicate, itemURI
|
||||
);
|
||||
for (let i=0; i<reverseRelations.length; i++) {
|
||||
let rel = reverseRelations[i];
|
||||
relations.push([rel.predicate, rel.subject]);
|
||||
}
|
||||
|
||||
this._relations = relations;
|
||||
this._loaded.relations = true;
|
||||
this._clearChanged('relations');
|
||||
/**
|
||||
* Return an item in the specified library equivalent to this item
|
||||
*
|
||||
* @return {Promise<Zotero.Item>}
|
||||
*/
|
||||
Zotero.Item.prototype.getLinkedItem = function (libraryID, bidirectional) {
|
||||
return this._getLinkedObject(libraryID, bidirectional);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Add a linked-object relation pointing to the given item
|
||||
*
|
||||
* Does not require a separate save()
|
||||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
Zotero.Item.prototype.addLinkedItem = Zotero.Promise.coroutine(function* (item) {
|
||||
return this._addLinkedObject(item);
|
||||
});
|
||||
|
||||
|
||||
|
@ -4633,21 +4453,16 @@ Zotero.Item.prototype.loadRelations = Zotero.Promise.coroutine(function* (reload
|
|||
//
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
/**
|
||||
* Returns related items this item point to
|
||||
* Returns related items this item points to
|
||||
*
|
||||
* @return {String[]} - An array of item keys
|
||||
* @return {String[]} - Keys of related items
|
||||
*/
|
||||
Zotero.Item.prototype._getRelatedItems = function () {
|
||||
this._requireData('relations');
|
||||
|
||||
var predicate = Zotero.Relations.relatedItemPredicate;
|
||||
|
||||
var relatedItemURIs = this.getRelations()[predicate];
|
||||
if (!relatedItemURIs) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (typeof relatedItemURIs == 'string') relatedItemURIs = [relatedItemURIs];
|
||||
var relatedItemURIs = this.getRelationsByPredicate(predicate);
|
||||
|
||||
// Pull out object values from related-item relations, turn into items, and pull out keys
|
||||
var keys = [];
|
||||
|
@ -4661,83 +4476,6 @@ Zotero.Item.prototype._getRelatedItems = function () {
|
|||
}
|
||||
|
||||
|
||||
|
||||
Zotero.Item.prototype._setRelatedItems = Zotero.Promise.coroutine(function* (itemIDs) {
|
||||
if (!this._loaded.relatedItems) {
|
||||
yield this._loadRelatedItems();
|
||||
}
|
||||
|
||||
if (itemIDs.constructor.name != 'Array') {
|
||||
throw ('ids must be an array in Zotero.Items._setRelatedItems()');
|
||||
}
|
||||
|
||||
var currentIDs = this._getRelatedItems(true);
|
||||
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 = yield this.ObjectsClass.getAsync(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._markFieldChange('related', currentIDs);
|
||||
this._changed.relatedItems = true
|
||||
}
|
||||
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(yield this.ObjectsClass.getAsync(itemID));
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* @return {Object} Return a copy of the creators, with additional 'id' properties
|
||||
*/
|
||||
|
|
|
@ -410,12 +410,16 @@ Zotero.Items = function() {
|
|||
|
||||
yield item.loadTags();
|
||||
yield item.loadRelations();
|
||||
var replPred = Zotero.Relations.replacedItemPredicate;
|
||||
var toSave = {};
|
||||
toSave[this.id];
|
||||
|
||||
for each(var otherItem in otherItems) {
|
||||
yield otherItem.loadChildItems();
|
||||
yield otherItem.loadCollections();
|
||||
yield otherItem.loadTags();
|
||||
yield otherItem.loadRelations();
|
||||
let otherItemURI = Zotero.URI.getItemURI(otherItem);
|
||||
|
||||
// Move child items to master
|
||||
var ids = otherItem.getAttachments(true).concat(otherItem.getNotes(true));
|
||||
|
@ -428,6 +432,34 @@ Zotero.Items = function() {
|
|||
yield attachment.save();
|
||||
}
|
||||
|
||||
// Add relations to master
|
||||
item.setRelations(otherItem.getRelations());
|
||||
|
||||
// Remove merge-tracking relations from other item, so that there aren't two
|
||||
// subjects for a given deleted object
|
||||
let replItems = otherItem.getRelationsByPredicate(replPred);
|
||||
for (let replItem of replItems) {
|
||||
otherItem.removeRelation(replPred, replItem);
|
||||
}
|
||||
|
||||
// Update relations on items in the library that point to the other item
|
||||
// to point to the master instead
|
||||
let rels = yield Zotero.Relations.getByObject('item', otherItemURI);
|
||||
for (let rel of rels) {
|
||||
// Skip merge-tracking relations, which are dealt with above
|
||||
if (rel.predicate == replPred) continue;
|
||||
// Skip items in other libraries. They might not be editable, and even
|
||||
// if they are, merging items in one library shouldn't affect another library,
|
||||
// so those will follow the merge-tracking relations and can optimize their
|
||||
// path if they're resaved.
|
||||
if (rel.subject.libraryID != item.libraryID) continue;
|
||||
rel.subject.removeRelation(rel.predicate, otherItemURI);
|
||||
rel.subject.addRelation(rel.predicate, itemURI);
|
||||
if (!toSave[rel.subject.id]) {
|
||||
toSave[rel.subject.id] = rel.subject;
|
||||
}
|
||||
}
|
||||
|
||||
// All other operations are additive only and do not affect the,
|
||||
// old item, which will be put in the trash
|
||||
|
||||
|
@ -444,34 +476,17 @@ Zotero.Items = function() {
|
|||
item.addTag(tags[j].tag);
|
||||
}
|
||||
|
||||
// Related items
|
||||
var relatedItems = otherItem.relatedItems;
|
||||
for each(var relatedItemID in relatedItems) {
|
||||
yield item.addRelatedItem(relatedItemID);
|
||||
}
|
||||
|
||||
// Relations
|
||||
yield Zotero.Relations.copyURIs(
|
||||
item.libraryID,
|
||||
Zotero.URI.getItemURI(otherItem),
|
||||
Zotero.URI.getItemURI(item)
|
||||
);
|
||||
|
||||
// Add relation to track merge
|
||||
var otherItemURI = Zotero.URI.getItemURI(otherItem);
|
||||
yield Zotero.Relations.add(
|
||||
item.libraryID,
|
||||
otherItemURI,
|
||||
Zotero.Relations.deletedItemPredicate,
|
||||
itemURI
|
||||
);
|
||||
item.addRelation(replPred, otherItemURI);
|
||||
|
||||
// Trash other item
|
||||
otherItem.deleted = true;
|
||||
yield otherItem.save();
|
||||
}
|
||||
|
||||
yield item.save();
|
||||
for (let i in toSave) {
|
||||
yield toSave[i].save();
|
||||
}
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
|
|
|
@ -23,77 +23,66 @@
|
|||
***** END LICENSE BLOCK *****
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
Zotero.Relations = new function () {
|
||||
Zotero.defineProperty(this, 'relatedItemPredicate', {value: 'dc:relation'});
|
||||
Zotero.defineProperty(this, 'linkedObjectPredicate', {value: 'owl:sameAs'});
|
||||
Zotero.defineProperty(this, 'deletedItemPredicate', {value: 'dc:isReplacedBy'});
|
||||
Zotero.defineProperty(this, 'replacedItemPredicate', {value: 'dc:replaces'});
|
||||
|
||||
this._namespaces = {
|
||||
dc: 'http://purl.org/dc/elements/1.1/',
|
||||
owl: 'http://www.w3.org/2002/07/owl#'
|
||||
};
|
||||
|
||||
var _types = ['collection', 'item'];
|
||||
|
||||
|
||||
/**
|
||||
* @return {Object[]}
|
||||
* Get the data objects that are subjects with the given predicate and object
|
||||
*
|
||||
* @param {String} objectType - Type of relation to search for (e.g., 'item')
|
||||
* @param {String} predicate
|
||||
* @param {String} object
|
||||
* @return {Promise<Zotero.DataObject[]>}
|
||||
*/
|
||||
this.getByURIs = Zotero.Promise.coroutine(function* (subject, predicate, object) {
|
||||
this.getByPredicateAndObject = Zotero.Promise.coroutine(function* (objectType, predicate, object) {
|
||||
var objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(objectType);
|
||||
if (predicate) {
|
||||
predicate = this._getPrefixAndValue(predicate).join(':');
|
||||
}
|
||||
|
||||
if (!subject && !predicate && !object) {
|
||||
throw new Error("No values provided");
|
||||
}
|
||||
|
||||
var sql = "SELECT ROWID FROM relations WHERE 1";
|
||||
var params = [];
|
||||
if (subject) {
|
||||
sql += " AND subject=?";
|
||||
params.push(subject);
|
||||
}
|
||||
if (predicate) {
|
||||
sql += " AND predicate=?";
|
||||
params.push(predicate);
|
||||
}
|
||||
if (object) {
|
||||
sql += " AND object=?";
|
||||
params.push(object);
|
||||
}
|
||||
var rows = yield Zotero.DB.columnQueryAsync(sql, params);
|
||||
var sql = "SELECT " + objectsClass.idColumn + " FROM " + objectType + "Relations "
|
||||
+ "JOIN relationPredicates USING (predicateID) WHERE predicate=? AND object=?";
|
||||
var ids = yield Zotero.DB.columnQueryAsync(sql, [predicate, object]);
|
||||
return yield objectsClass.getAsync(ids, { noCache: true });
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Get the data objects that are subjects with the given predicate and object
|
||||
*
|
||||
* @param {String} objectType - Type of relation to search for (e.g., 'item')
|
||||
* @param {String} object
|
||||
* @return {Promise<Object[]>} - Promise for an object with a Zotero.DataObject as 'subject'
|
||||
* and a predicate string as 'predicate'
|
||||
*/
|
||||
this.getByObject = Zotero.Promise.coroutine(function* (objectType, object) {
|
||||
var objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(objectType);
|
||||
var sql = "SELECT " + objectsClass.idColumn + " AS id, predicate "
|
||||
+ "FROM " + objectType + "Relations JOIN relationPredicates USING (predicateID) "
|
||||
+ "WHERE object=?";
|
||||
var toReturn = [];
|
||||
for (let i=0; i<rows.length; i++) {
|
||||
let row = rows[i];
|
||||
var rows = yield Zotero.DB.queryAsync(sql, object);
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
toReturn.push({
|
||||
subject: row.subject,
|
||||
predicate: row.predicate,
|
||||
object: row.object
|
||||
subject: yield objectsClass.getAsync(rows[i].id, { noCache: true }),
|
||||
predicate: rows[i].predicate
|
||||
});
|
||||
}
|
||||
return toReturn;
|
||||
});
|
||||
|
||||
|
||||
this.getSubject = Zotero.Promise.coroutine(function* (subject, predicate, object) {
|
||||
var subjects = [];
|
||||
var relations = yield this.getByURIs(subject, predicate, object);
|
||||
for each(var relation in relations) {
|
||||
subjects.push(relation.subject);
|
||||
}
|
||||
return subjects;
|
||||
});
|
||||
|
||||
|
||||
this.getObject = Zotero.Promise.coroutine(function* (subject, predicate, object) {
|
||||
var objects = [];
|
||||
var relations = yield this.getByURIs(subject, predicate, object);
|
||||
for each(var relation in relations) {
|
||||
objects.push(relation.object);
|
||||
}
|
||||
return objects;
|
||||
});
|
||||
|
||||
|
||||
this.updateUser = Zotero.Promise.coroutine(function* (fromUserID, fromLibraryID, toUserID, toLibraryID) {
|
||||
if (!fromUserID) {
|
||||
throw ("Invalid source userID " + fromUserID + " in Zotero.Relations.updateUserID");
|
||||
|
@ -118,104 +107,53 @@ Zotero.Relations = new function () {
|
|||
+ "object=REPLACE(object, 'zotero.org/users/" + fromUserID + "', "
|
||||
+ "'zotero.org/users/" + toUserID + "') "
|
||||
+ "WHERE predicate IN (?, ?)";
|
||||
yield Zotero.DB.queryAsync(sql, [this.linkedObjectPredicate, this.deletedItemPredicate]);
|
||||
yield Zotero.DB.queryAsync(sql, [this.linkedObjectPredicate, this.replacedItemPredicate]);
|
||||
}.bind(this));
|
||||
});
|
||||
|
||||
|
||||
this.add = Zotero.Promise.coroutine(function* (libraryID, subject, predicate, object) {
|
||||
predicate = this._getPrefixAndValue(predicate).join(':');
|
||||
var sql = "INSERT INTO relations (libraryID, subject, predicate, object) "
|
||||
+ "VALUES (?, ?, ?, ?)";
|
||||
yield Zotero.DB.queryAsync(sql, [libraryID, subject, predicate, object]);
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Copy relations from one object to another within the same library
|
||||
*/
|
||||
this.copyURIs = Zotero.Promise.coroutine(function* (libraryID, fromURI, toURI) {
|
||||
var rels = yield this.getByURIs(fromURI);
|
||||
for each(var rel in rels) {
|
||||
yield this.add(libraryID, toURI, rel.predicate, rel.object);
|
||||
}
|
||||
|
||||
var rels = yield this.getByURIs(false, false, fromURI);
|
||||
for each(var rel in rels) {
|
||||
yield this.add(libraryID, rel.subject, rel.predicate, toURI);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Deletes relations directly from the DB by URI prefix
|
||||
*
|
||||
* This does not update associated objects.
|
||||
*
|
||||
* @param {String} prefix
|
||||
* @param {String[]} ignorePredicates
|
||||
*/
|
||||
this.eraseByURIPrefix = Zotero.Promise.method(function (prefix, ignorePredicates) {
|
||||
prefix = prefix + '%';
|
||||
var sql = "DELETE FROM relations WHERE (subject LIKE ? OR object LIKE ?)";
|
||||
var params = [prefix, prefix];
|
||||
if (ignorePredicates) {
|
||||
for each(var ignorePredicate in ignorePredicates) {
|
||||
sql += " AND predicate != ?";
|
||||
params.push(ignorePredicate);
|
||||
}
|
||||
}
|
||||
yield Zotero.DB.queryAsync(sql, params);
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Deletes relations directly from the DB by URI prefix
|
||||
*
|
||||
* This does not update associated objects.
|
||||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
this.eraseByURI = Zotero.Promise.coroutine(function* (uri, ignorePredicates) {
|
||||
var sql = "DELETE FROM relations WHERE (subject=? OR object=?)";
|
||||
var params = [uri, uri];
|
||||
if (ignorePredicates) {
|
||||
for each(var ignorePredicate in ignorePredicates) {
|
||||
sql += " AND predicate != ?";
|
||||
params.push(ignorePredicate);
|
||||
}
|
||||
}
|
||||
yield Zotero.DB.queryAsync(sql, params);
|
||||
});
|
||||
|
||||
|
||||
this.purge = Zotero.Promise.coroutine(function* () {
|
||||
Zotero.DB.requireTransaction();
|
||||
|
||||
Zotero.debug("Purging relations");
|
||||
|
||||
Zotero.DB.requireTransaction();
|
||||
var t = new Date;
|
||||
var sql = "SELECT subject FROM relations WHERE predicate != ? "
|
||||
+ "UNION SELECT object FROM relations WHERE predicate != ?";
|
||||
var uris = yield Zotero.DB.columnQueryAsync(sql, [this.deletedItemPredicate, this.deletedItemPredicate]);
|
||||
if (uris) {
|
||||
var prefix = Zotero.URI.defaultPrefix;
|
||||
for each(var uri in uris) {
|
||||
// Skip URIs that don't begin with the default prefix,
|
||||
// since they don't correspond to local items
|
||||
if (uri.indexOf(prefix) == -1) {
|
||||
continue;
|
||||
let prefix = Zotero.URI.defaultPrefix;
|
||||
var types = ['collection', 'item'];
|
||||
for (let type of types) {
|
||||
let objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type);
|
||||
let getFunc = "getURI" + Zotero.Utilities.capitalize(type);
|
||||
let objects = {};
|
||||
|
||||
// Get all object URIs except merge-tracking ones
|
||||
let sql = "SELECT " + objectsClass.idColumn + " AS id, predicate, object "
|
||||
+ "FROM " + type + "Relations "
|
||||
+ "JOIN relationPredicates USING (predicateID) WHERE predicate != ?";
|
||||
let rows = yield Zotero.DB.queryAsync(sql, [this.replacedItemPredicate]);
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
let row = rows[i];
|
||||
let uri = row.object;
|
||||
// Erase Zotero URIs of this type that don't resolve to a local object
|
||||
//
|
||||
// TODO: Check for replaced-item relations and update relation rather than
|
||||
// removing
|
||||
if (uri.indexOf(prefix) != -1
|
||||
&& uri.indexOf("/" + type + "s/") != -1
|
||||
&& !Zotero.URI[getFunc](uri)) {
|
||||
if (!objects[row.id]) {
|
||||
objects[row.id] = yield objectsClass.getAsync(row.id, { noCache: true });
|
||||
}
|
||||
objects[row.id].removeRelation(row.predicate, uri);
|
||||
}
|
||||
if (uri.indexOf("/items/") != -1 && !Zotero.URI.getURIItemID(uri)) {
|
||||
yield this.eraseByURI(uri);
|
||||
}
|
||||
if (uri.indexOf("/collections/") != -1 && !Zotero.URI.getURICollectionID(uri)) {
|
||||
yield this.eraseByURI(uri);
|
||||
for (let i in objects) {
|
||||
yield objects[i].save();
|
||||
}
|
||||
}
|
||||
|
||||
Zotero.debug("Purged relations in " + ((new Date) - t) + "ms");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
this._getPrefixAndValue = function(uri) {
|
||||
var [prefix, value] = uri.split(':');
|
||||
if (prefix && value) {
|
||||
|
|
|
@ -3118,7 +3118,7 @@ Zotero.Integration.URIMap.prototype.getZoteroItemForURIs = function(uris) {
|
|||
|
||||
// Follow merged item relations until we find an item or hit a dead end
|
||||
while (!zoteroItem) {
|
||||
var relations = Zotero.Relations.getByURIs(uri, Zotero.Relations.deletedItemPredicate);
|
||||
var relations = Zotero.Relations.getByURIs(uri, Zotero.Relations.replacedItemPredicate);
|
||||
// No merged items found
|
||||
if(!relations.length) {
|
||||
break;
|
||||
|
|
|
@ -1938,8 +1938,6 @@ Zotero.Schema = new function(){
|
|||
yield Zotero.DB.queryAsync("INSERT INTO libraries VALUES (1, 'user')");
|
||||
yield Zotero.DB.queryAsync("INSERT INTO libraries VALUES (2, 'publications')");
|
||||
|
||||
let oldUserLibraryID = yield Zotero.DB.valueQueryAsync("SELECT value FROM settings WHERE setting='account' AND key='libraryID'");
|
||||
|
||||
yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO syncObjectTypes VALUES (7, 'setting')");
|
||||
yield Zotero.DB.queryAsync("DELETE FROM version WHERE schema IN ('userdata2', 'userdata3')");
|
||||
|
||||
|
@ -2152,12 +2150,6 @@ Zotero.Schema = new function(){
|
|||
yield Zotero.DB.queryAsync("DROP INDEX IF EXISTS itemAttachments_syncState");
|
||||
yield Zotero.DB.queryAsync("CREATE INDEX itemAttachments_syncState ON itemAttachments(syncState)");
|
||||
|
||||
yield Zotero.DB.queryAsync("ALTER TABLE itemSeeAlso RENAME TO itemSeeAlsoOld");
|
||||
yield Zotero.DB.queryAsync("CREATE TABLE itemSeeAlso (\n itemID INT NOT NULL,\n linkedItemID INT NOT NULL,\n PRIMARY KEY (itemID, linkedItemID),\n FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE,\n FOREIGN KEY (linkedItemID) REFERENCES items(itemID) ON DELETE CASCADE\n)");
|
||||
yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO itemSeeAlso SELECT * FROM itemSeeAlsoOld");
|
||||
yield Zotero.DB.queryAsync("DROP INDEX IF EXISTS itemSeeAlso_linkedItemID");
|
||||
yield Zotero.DB.queryAsync("CREATE INDEX itemSeeAlso_linkedItemID ON itemSeeAlso(linkedItemID)");
|
||||
|
||||
yield Zotero.DB.queryAsync("ALTER TABLE collectionItems RENAME TO collectionItemsOld");
|
||||
yield Zotero.DB.queryAsync("CREATE TABLE collectionItems (\n collectionID INT NOT NULL,\n itemID INT NOT NULL,\n orderIndex INT NOT NULL DEFAULT 0,\n PRIMARY KEY (collectionID, itemID),\n FOREIGN KEY (collectionID) REFERENCES collections(collectionID) ON DELETE CASCADE,\n FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE\n)");
|
||||
yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO collectionItems SELECT * FROM collectionItemsOld");
|
||||
|
@ -2175,12 +2167,7 @@ Zotero.Schema = new function(){
|
|||
yield Zotero.DB.queryAsync("DROP INDEX IF EXISTS deletedItems_dateDeleted");
|
||||
yield Zotero.DB.queryAsync("CREATE INDEX deletedItems_dateDeleted ON deletedItems(dateDeleted)");
|
||||
|
||||
yield Zotero.DB.queryAsync("UPDATE relations SET libraryID=1 WHERE libraryID=?", oldUserLibraryID);
|
||||
yield Zotero.DB.queryAsync("ALTER TABLE relations RENAME TO relationsOld");
|
||||
yield Zotero.DB.queryAsync("CREATE TABLE relations (\n libraryID INT NOT NULL,\n subject TEXT NOT NULL,\n predicate TEXT NOT NULL,\n object TEXT NOT NULL,\n clientDateModified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n PRIMARY KEY (subject, predicate, object),\n FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE\n)");
|
||||
yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO relations SELECT * FROM relationsOld");
|
||||
yield Zotero.DB.queryAsync("DROP INDEX IF EXISTS relations_object");
|
||||
yield Zotero.DB.queryAsync("CREATE INDEX relations_object ON relations(object)");
|
||||
yield _migrateUserData_80_relations();
|
||||
|
||||
yield Zotero.DB.queryAsync("ALTER TABLE groups RENAME TO groupsOld");
|
||||
yield Zotero.DB.queryAsync("CREATE TABLE groups (\n groupID INTEGER PRIMARY KEY,\n libraryID INT NOT NULL UNIQUE,\n name TEXT NOT NULL,\n description TEXT NOT NULL,\n editable INT NOT NULL,\n filesEditable INT NOT NULL,\n version INT NOT NULL,\n FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE\n)");
|
||||
|
@ -2256,9 +2243,7 @@ Zotero.Schema = new function(){
|
|||
yield Zotero.DB.queryAsync("DROP TABLE itemCreatorsOld");
|
||||
yield Zotero.DB.queryAsync("DROP TABLE itemDataOld");
|
||||
yield Zotero.DB.queryAsync("DROP TABLE itemNotesOld");
|
||||
yield Zotero.DB.queryAsync("DROP TABLE itemSeeAlsoOld");
|
||||
yield Zotero.DB.queryAsync("DROP TABLE itemTagsOld");
|
||||
yield Zotero.DB.queryAsync("DROP TABLE relationsOld");
|
||||
yield Zotero.DB.queryAsync("DROP TABLE savedSearchesOld");
|
||||
yield Zotero.DB.queryAsync("DROP TABLE storageDeleteLogOld");
|
||||
yield Zotero.DB.queryAsync("DROP TABLE syncDeleteLogOld");
|
||||
|
@ -2274,4 +2259,203 @@ Zotero.Schema = new function(){
|
|||
yield _updateDBVersion('userdata', toVersion);
|
||||
return true;
|
||||
});
|
||||
|
||||
|
||||
//
|
||||
// Longer functions for specific upgrade steps
|
||||
//
|
||||
var _migrateUserData_80_relations = Zotero.Promise.coroutine(function* () {
|
||||
yield Zotero.DB.queryAsync("CREATE TABLE relationPredicates (\n predicateID INTEGER PRIMARY KEY,\n predicate TEXT UNIQUE\n)");
|
||||
|
||||
yield Zotero.DB.queryAsync("CREATE TABLE collectionRelations (\n collectionID INT NOT NULL,\n predicateID INT NOT NULL,\n object TEXT NOT NULL,\n PRIMARY KEY (collectionID, predicateID, object),\n FOREIGN KEY (collectionID) REFERENCES collections(collectionID) ON DELETE CASCADE,\n FOREIGN KEY (predicateID) REFERENCES relationPredicates(predicateID) ON DELETE CASCADE\n)");
|
||||
yield Zotero.DB.queryAsync("CREATE INDEX collectionRelations_predicateID ON collectionRelations(predicateID)");
|
||||
yield Zotero.DB.queryAsync("CREATE INDEX collectionRelations_object ON collectionRelations(object);");
|
||||
yield Zotero.DB.queryAsync("CREATE TABLE itemRelations (\n itemID INT NOT NULL,\n predicateID INT NOT NULL,\n object TEXT NOT NULL,\n PRIMARY KEY (itemID, predicateID, object),\n FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE,\n FOREIGN KEY (predicateID) REFERENCES relationPredicates(predicateID) ON DELETE CASCADE\n)");
|
||||
yield Zotero.DB.queryAsync("CREATE INDEX itemRelations_predicateID ON itemRelations(predicateID)");
|
||||
yield Zotero.DB.queryAsync("CREATE INDEX itemRelations_object ON itemRelations(object);");
|
||||
|
||||
yield Zotero.DB.queryAsync("UPDATE relations SET subject=object, predicate='dc:replaces', object=subject WHERE predicate='dc:isReplacedBy'");
|
||||
|
||||
var start = 0;
|
||||
var limit = 100;
|
||||
var collectionSQL = "INSERT OR IGNORE INTO collectionRelations (collectionID, predicateID, object) VALUES ";
|
||||
var itemSQL = "INSERT OR IGNORE INTO itemRelations (itemID, predicateID, object) VALUES ";
|
||||
// 1 2 1 2 3 4
|
||||
var objectRE = /(?:(users)\/(\d+|local\/\w+)|(groups)\/(\d+))\/(collections|items)\/([A-Z0-9]{8})/;
|
||||
// 1 2 1 2 3
|
||||
var itemRE = /(?:(users)\/(\d+|local\/\w+)|(groups)\/(\d+))\/items\/([A-Z0-9]{8})/;
|
||||
var report = "";
|
||||
var groupLibraryIDMap = {};
|
||||
var resolveLibrary = Zotero.Promise.coroutine(function* (usersOrGroups, id) {
|
||||
if (usersOrGroups == 'users') return 1;
|
||||
if (groupLibraryIDMap[id] !== undefined) return groupLibraryIDMap[id];
|
||||
return groupLibraryIDMap[id] = (yield Zotero.DB.valueQueryAsync("SELECT libraryID FROM groups WHERE libraryID=?", id));
|
||||
});
|
||||
var predicateMap = {};
|
||||
var resolvePredicate = Zotero.Promise.coroutine(function* (predicate) {
|
||||
if (predicateMap[predicate]) return predicateMap[predicate];
|
||||
yield Zotero.DB.queryAsync("INSERT INTO relationPredicates (predicateID, predicate) VALUES (NULL, ?)", predicate);
|
||||
return predicateMap[predicate] = Zotero.DB.valueQueryAsync("SELECT predicateID FROM relationPredicates WHERE predicate=?", predicate);
|
||||
});
|
||||
while (true) {
|
||||
let rows = yield Zotero.DB.queryAsync("SELECT subject, predicate, object FROM relations LIMIT ?, ?", [start, limit]);
|
||||
if (!rows.length) {
|
||||
break;
|
||||
}
|
||||
|
||||
let collectionRels = [];
|
||||
let itemRels = [];
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
let row = rows[i];
|
||||
let concat = row.subject + " - " + row.predicate + " - " + row.object;
|
||||
|
||||
try {
|
||||
switch (row.predicate) {
|
||||
case 'owl:sameAs':
|
||||
let subjectMatch = row.subject.match(objectRE);
|
||||
let objectMatch = row.object.match(objectRE);
|
||||
if (!subjectMatch && !objectMatch) {
|
||||
Zotero.logError("No match for relation subject or object: " + concat);
|
||||
report += concat + "\n";
|
||||
continue;
|
||||
}
|
||||
// Remove empty captured groups
|
||||
subjectMatch = subjectMatch ? subjectMatch.filter(x => x) : false;
|
||||
objectMatch = objectMatch ? objectMatch.filter(x => x) : false;
|
||||
let subjectLibraryID = false;
|
||||
let subjectType = false;
|
||||
let subject = false;
|
||||
let objectLibraryID = false;
|
||||
let objectType = false;
|
||||
let object = false;
|
||||
if (subjectMatch) {
|
||||
subjectLibraryID = (yield resolveLibrary(subjectMatch[1], subjectMatch[2])) || false;
|
||||
subjectType = subjectMatch[3];
|
||||
}
|
||||
if (objectMatch) {
|
||||
objectLibraryID = (yield resolveLibrary(objectMatch[1], objectMatch[2])) || false;
|
||||
objectType = objectMatch[3];
|
||||
}
|
||||
// Use subject if it's a user library or it isn't but neither is object, and if object can be found
|
||||
if (subjectLibraryID && (subjectLibraryID == 1 || objectLibraryID != 1)) {
|
||||
let key = subjectMatch[4];
|
||||
if (subjectType == 'collection') {
|
||||
let collectionID = yield Zotero.DB.valueQueryAsync("SELECT collectionID FROM collections WHERE libraryID=? AND key=?", [subjectLibraryID, key]);
|
||||
if (collectionID) {
|
||||
collectionRels.push([collectionID, row.predicate, row.object]);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else {
|
||||
let itemID = yield Zotero.DB.valueQueryAsync("SELECT itemID FROM items WHERE libraryID=? AND key=?", [subjectLibraryID, key]);
|
||||
if (itemID) {
|
||||
itemRels.push([itemID, row.predicate, row.object]);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise use object if it can be found
|
||||
if (objectLibraryID) {
|
||||
let key = objectMatch[4];
|
||||
if (objectType == 'collection') {
|
||||
let collectionID = yield Zotero.DB.valueQueryAsync("SELECT collectionID FROM collections WHERE libraryID=? AND key=?", [objectLibraryID, key]);
|
||||
if (collectionID) {
|
||||
collectionRels.push([collectionID, row.predicate, row.subject]);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else {
|
||||
let itemID = yield Zotero.DB.valueQueryAsync("SELECT itemID FROM items WHERE libraryID=? AND key=?", [objectLibraryID, key]);
|
||||
if (itemID) {
|
||||
itemRels.push([itemID, row.predicate, row.subject]);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Zotero.logError("Neither subject nor object found: " + concat);
|
||||
report += concat + "\n";
|
||||
}
|
||||
break;
|
||||
|
||||
case 'dc:replaces':
|
||||
let match = row.subject.match(itemRE);
|
||||
if (!match) {
|
||||
Zotero.logError("Unrecognized subject: " + concat);
|
||||
report += concat + "\n";
|
||||
continue;
|
||||
}
|
||||
// Remove empty captured groups
|
||||
match = match.filter(x => x);
|
||||
let libraryID;
|
||||
// Users
|
||||
if (match[1] == 'users') {
|
||||
let itemID = yield Zotero.DB.valueQueryAsync("SELECT itemID FROM items WHERE libraryID=? AND key=?", [1, match[3]]);
|
||||
if (!itemID) {
|
||||
Zotero.logError("Subject not found: " + concat);
|
||||
report += concat + "\n";
|
||||
continue;
|
||||
}
|
||||
itemRels.push([itemID, row.predicate, row.object]);
|
||||
}
|
||||
// Groups
|
||||
else {
|
||||
let itemID = yield Zotero.DB.valueQueryAsync("SELECT itemID FROM items JOIN groups USING (libraryID) WHERE groupID=? AND key=?", [match[2], match[3]]);
|
||||
if (!itemID) {
|
||||
Zotero.logError("Subject not found: " + concat);
|
||||
report += concat + "\n";
|
||||
continue;
|
||||
}
|
||||
itemRels.push([itemID, row.predicate, row.object]);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
Zotero.logError("Unknown predicate '" + row.predicate + "': " + concat);
|
||||
report += concat + "\n";
|
||||
continue;
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.logError(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (collectionRels.length) {
|
||||
for (let i = 0; i < collectionRels.length; i++) {
|
||||
collectionRels[i][1] = yield resolvePredicate(collectionRels[i][1]);
|
||||
}
|
||||
yield Zotero.DB.queryAsync(collectionSQL + collectionRels.map(() => "(?, ?, ?)").join(", "), collectionRels.reduce((x, y) => x.concat(y)));
|
||||
}
|
||||
if (itemRels.length) {
|
||||
for (let i = 0; i < itemRels.length; i++) {
|
||||
itemRels[i][1] = yield resolvePredicate(itemRels[i][1]);
|
||||
}
|
||||
yield Zotero.DB.queryAsync(itemSQL + itemRels.map(() => "(?, ?, ?)").join(", "), itemRels.reduce((x, y) => x.concat(y)));
|
||||
}
|
||||
|
||||
start += limit;
|
||||
}
|
||||
if (report.length) {
|
||||
report = "Removed relations:\n\n" + report;
|
||||
Zotero.debug(report);
|
||||
}
|
||||
yield Zotero.DB.queryAsync("DROP TABLE relations");
|
||||
|
||||
//
|
||||
// Migrate related items
|
||||
//
|
||||
// If no user id and no local key, create a local key
|
||||
if (!(yield Zotero.DB.valueQueryAsync("SELECT value FROM settings WHERE setting='account' AND key='userID'"))
|
||||
&& !(yield Zotero.DB.valueQueryAsync("SELECT value FROM settings WHERE setting='account' AND key='localUserKey'"))) {
|
||||
yield Zotero.DB.queryAsync("INSERT INTO settings (setting, key, value) VALUES ('account', 'localUserKey', ?)", Zotero.randomString(8));
|
||||
}
|
||||
var predicateID = predicateMap["dc:relation"];
|
||||
if (!predicateID) {
|
||||
yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO relationPredicates VALUES (NULL, 'dc:relation')");
|
||||
predicateID = yield Zotero.DB.valueQueryAsync("SELECT predicateID FROM relationPredicates WHERE predicate=?", 'dc:relation');
|
||||
}
|
||||
yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO itemRelations SELECT ISA.itemID, " + predicateID + ", 'http://zotero.org/' || (CASE WHEN G.libraryID IS NULL THEN 'users/' || IFNULL((SELECT value FROM settings WHERE setting='account' AND key='userID'), (SELECT value FROM settings WHERE setting='account' AND key='localUserKey')) ELSE 'groups/' || G.groupID END) || '/' || I.key FROM itemSeeAlso ISA JOIN items I ON (ISA.linkedItemID=I.itemID) LEFT JOIN groups G USING (libraryID)");
|
||||
yield Zotero.DB.queryAsync("DROP TABLE itemSeeAlso");
|
||||
});
|
||||
}
|
||||
|
|
|
@ -592,8 +592,9 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
|
|||
yield Zotero.ItemTypes.init();
|
||||
yield Zotero.ItemFields.init();
|
||||
yield Zotero.CreatorTypes.init();
|
||||
yield Zotero.CharacterSets.init();
|
||||
yield Zotero.FileTypes.init();
|
||||
yield Zotero.CharacterSets.init();
|
||||
yield Zotero.RelationPredicates.init();
|
||||
|
||||
Zotero.locked = false;
|
||||
|
||||
|
|
|
@ -111,9 +111,9 @@ customcolorpicker[type=button] {
|
|||
-moz-binding: url(chrome://zotero/content/bindings/customcolorpicker.xml#custom-colorpicker-button);
|
||||
}
|
||||
|
||||
seealsobox
|
||||
relatedbox
|
||||
{
|
||||
-moz-binding: url('chrome://zotero/content/bindings/relatedbox.xml#seealso-box');
|
||||
-moz-binding: url('chrome://zotero/content/bindings/relatedbox.xml#related-box');
|
||||
-moz-user-focus: ignore;
|
||||
}
|
||||
|
||||
|
|
|
@ -210,25 +210,6 @@ CREATE TRIGGER fku_itemNotes
|
|||
END;
|
||||
|
||||
|
||||
-- itemSeeAlso libraryID
|
||||
DROP TRIGGER IF EXISTS fki_itemSeeAlso_libraryID;
|
||||
CREATE TRIGGER fki_itemSeeAlso_libraryID
|
||||
BEFORE INSERT ON itemSeeAlso
|
||||
FOR EACH ROW BEGIN
|
||||
SELECT RAISE(ABORT, 'insert on table "itemSeeAlso" violates foreign key constraint "fki_itemSeeAlso_libraryID"')
|
||||
WHERE (SELECT libraryID FROM items WHERE itemID = NEW.itemID) != (SELECT libraryID FROM items WHERE itemID = NEW.linkedItemID);---
|
||||
END;
|
||||
|
||||
DROP TRIGGER IF EXISTS fku_itemSeeAlso_libraryID;
|
||||
CREATE TRIGGER fku_itemSeeAlso_libraryID
|
||||
BEFORE UPDATE ON itemSeeAlso
|
||||
FOR EACH ROW BEGIN
|
||||
SELECT RAISE(ABORT, 'update on table "itemSeeAlso" violates foreign key constraint "fku_itemSeeAlso_libraryID"')
|
||||
WHERE (SELECT libraryID FROM items WHERE itemID = NEW.itemID) != (SELECT libraryID FROM items WHERE itemID = NEW.linkedItemID);---
|
||||
END;
|
||||
|
||||
|
||||
|
||||
-- itemTags libraryID
|
||||
DROP TRIGGER IF EXISTS fki_itemTags_libraryID;
|
||||
CREATE TRIGGER fki_itemTags_libraryID
|
||||
|
|
|
@ -126,6 +126,17 @@ CREATE TABLE tags (
|
|||
UNIQUE (libraryID, name)
|
||||
);
|
||||
|
||||
CREATE TABLE itemRelations (
|
||||
itemID INT NOT NULL,
|
||||
predicateID INT NOT NULL,
|
||||
object TEXT NOT NULL,
|
||||
PRIMARY KEY (itemID, predicateID, object),
|
||||
FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE,
|
||||
FOREIGN KEY (predicateID) REFERENCES relationPredicates(predicateID) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX itemRelations_predicateID ON itemRelations(predicateID);
|
||||
CREATE INDEX itemRelations_object ON itemRelations(object);
|
||||
|
||||
CREATE TABLE itemTags (
|
||||
itemID INT NOT NULL,
|
||||
tagID INT NOT NULL,
|
||||
|
@ -191,6 +202,17 @@ CREATE TABLE collectionItems (
|
|||
);
|
||||
CREATE INDEX collectionItems_itemID ON collectionItems(itemID);
|
||||
|
||||
CREATE TABLE collectionRelations (
|
||||
collectionID INT NOT NULL,
|
||||
predicateID INT NOT NULL,
|
||||
object TEXT NOT NULL,
|
||||
PRIMARY KEY (collectionID, predicateID, object),
|
||||
FOREIGN KEY (collectionID) REFERENCES collections(collectionID) ON DELETE CASCADE,
|
||||
FOREIGN KEY (predicateID) REFERENCES relationPredicates(predicateID) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX collectionRelations_predicateID ON collectionRelations(predicateID);
|
||||
CREATE INDEX collectionRelations_object ON collectionRelations(object);
|
||||
|
||||
CREATE TABLE savedSearches (
|
||||
savedSearchID INTEGER PRIMARY KEY,
|
||||
savedSearchName TEXT NOT NULL,
|
||||
|
@ -222,17 +244,6 @@ CREATE TABLE deletedItems (
|
|||
);
|
||||
CREATE INDEX deletedItems_dateDeleted ON deletedItems(dateDeleted);
|
||||
|
||||
CREATE TABLE relations (
|
||||
libraryID INT NOT NULL,
|
||||
subject TEXT NOT NULL,
|
||||
predicate TEXT NOT NULL,
|
||||
object TEXT NOT NULL,
|
||||
clientDateModified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (subject, predicate, object),
|
||||
FOREIGN KEY (libraryID) REFERENCES libraries(libraryID) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX relations_object ON relations(object);
|
||||
|
||||
CREATE TABLE libraries (
|
||||
libraryID INTEGER PRIMARY KEY,
|
||||
libraryType TEXT NOT NULL,
|
||||
|
@ -368,6 +379,10 @@ CREATE TABLE proxyHosts (
|
|||
);
|
||||
CREATE INDEX proxyHosts_proxyID ON proxyHosts(proxyID);
|
||||
|
||||
CREATE TABLE relationPredicates (
|
||||
predicateID INTEGER PRIMARY KEY,
|
||||
predicate TEXT UNIQUE
|
||||
);
|
||||
|
||||
-- These shouldn't be used yet
|
||||
CREATE TABLE customItemTypes (
|
||||
|
|
|
@ -89,8 +89,9 @@ function waitForWindow(uri, callback) {
|
|||
return deferred.promise;
|
||||
}
|
||||
|
||||
var selectLibrary = Zotero.Promise.coroutine(function* (win) {
|
||||
yield win.ZoteroPane.collectionsView.selectLibrary(Zotero.Libraries.userLibraryID);
|
||||
var selectLibrary = Zotero.Promise.coroutine(function* (win, libraryID) {
|
||||
libraryID = libraryID || Zotero.Libraries.userLibraryID;
|
||||
yield win.ZoteroPane.collectionsView.selectLibrary(libraryID);
|
||||
yield waitForItemsLoad(win);
|
||||
});
|
||||
|
||||
|
|
|
@ -189,14 +189,79 @@ describe("Zotero.CollectionTreeView", function() {
|
|||
})
|
||||
|
||||
describe("#drop()", function () {
|
||||
/**
|
||||
* Simulate a drag and drop
|
||||
*
|
||||
* @param {String} targetRowID - Tree row id (e.g., "L123")
|
||||
* @param {Integer[]} itemIDs
|
||||
* @param {Promise} [promise] - If a promise is provided, it will be waited for and its
|
||||
* value returned after the drag. Otherwise, an item 'add'
|
||||
* event will be waited for, and the added ids will be
|
||||
* returned.
|
||||
*/
|
||||
var drop = Zotero.Promise.coroutine(function* (targetRowID, itemIDs, promise) {
|
||||
var row = collectionsView.getRowIndexByID(targetRowID);
|
||||
|
||||
var stub = sinon.stub(Zotero.DragDrop, "getDragTarget");
|
||||
stub.returns(collectionsView.getRow(row));
|
||||
if (!promise) {
|
||||
promise = waitForItemEvent("add");
|
||||
}
|
||||
yield collectionsView.drop(row, 0, {
|
||||
dropEffect: 'copy',
|
||||
effectAllowed: 'copy',
|
||||
mozSourceNode: win.document.getElementById('zotero-items-tree'),
|
||||
types: {
|
||||
contains: function (type) {
|
||||
return type == 'zotero/item';
|
||||
}
|
||||
},
|
||||
getData: function (type) {
|
||||
if (type == 'zotero/item') {
|
||||
return itemIDs.join(",");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add observer to wait for add
|
||||
var result = yield promise;
|
||||
stub.restore();
|
||||
return result;
|
||||
});
|
||||
|
||||
|
||||
var canDrop = Zotero.Promise.coroutine(function* (targetRowID, itemIDs) {
|
||||
var row = collectionsView.getRowIndexByID(targetRowID);
|
||||
|
||||
var stub = sinon.stub(Zotero.DragDrop, "getDragTarget");
|
||||
stub.returns(collectionsView.getRow(row));
|
||||
var dt = {
|
||||
dropEffect: 'copy',
|
||||
effectAllowed: 'copy',
|
||||
mozSourceNode: win.document.getElementById('zotero-items-tree'),
|
||||
types: {
|
||||
contains: function (type) {
|
||||
return type == 'zotero/item';
|
||||
}
|
||||
},
|
||||
getData: function (type) {
|
||||
if (type == 'zotero/item') {
|
||||
return itemIDs.join(",");
|
||||
}
|
||||
}
|
||||
};
|
||||
var canDrop = collectionsView.canDropCheck(row, 0, dt);
|
||||
if (canDrop) {
|
||||
canDrop = yield collectionsView.canDropCheckAsync(row, 0, dt);
|
||||
}
|
||||
stub.restore();
|
||||
return canDrop;
|
||||
});
|
||||
|
||||
|
||||
it("should add an item to a collection", function* () {
|
||||
var collection = yield createDataObject('collection', false, {
|
||||
skipSelect: true
|
||||
});
|
||||
var item = yield createDataObject('item', false, {
|
||||
skipSelect: true
|
||||
});
|
||||
var row = collectionsView.getRowIndexByID("C" + collection.id);
|
||||
var collection = yield createDataObject('collection', false, { skipSelect: true });
|
||||
var item = yield createDataObject('item', false, { skipSelect: true });
|
||||
|
||||
// Add observer to wait for collection add
|
||||
var deferred = Zotero.Promise.defer();
|
||||
|
@ -211,27 +276,8 @@ describe("Zotero.CollectionTreeView", function() {
|
|||
}
|
||||
}, 'collection-item', 'test');
|
||||
|
||||
// Simulate a drag and drop
|
||||
var stub = sinon.stub(Zotero.DragDrop, "getDragTarget");
|
||||
stub.returns(collectionsView.getRow(row));
|
||||
collectionsView.drop(row, 0, {
|
||||
dropEffect: 'copy',
|
||||
effectAllowed: 'copy',
|
||||
mozSourceNode: win.document.getElementById('zotero-items-tree'),
|
||||
types: {
|
||||
contains: function (type) {
|
||||
return type == 'zotero/item';
|
||||
}
|
||||
},
|
||||
getData: function (type) {
|
||||
if (type == 'zotero/item') {
|
||||
return "" + item.id;
|
||||
}
|
||||
}
|
||||
})
|
||||
var ids = yield drop("C" + collection.id, [item.id], deferred.promise);
|
||||
|
||||
yield deferred.promise;
|
||||
stub.restore();
|
||||
Zotero.Notifier.unregisterObserver(observerID);
|
||||
yield collectionsView.selectCollection(collection.id);
|
||||
yield waitForItemsLoad(win);
|
||||
|
@ -242,60 +288,87 @@ describe("Zotero.CollectionTreeView", function() {
|
|||
assert.equal(treeRow.ref.id, item.id);
|
||||
})
|
||||
|
||||
it("should add an item to a library", function* () {
|
||||
var group = new Zotero.Group;
|
||||
group.id = 75161251;
|
||||
group.name = "Test";
|
||||
group.description = "";
|
||||
group.editable = true;
|
||||
group.filesEditable = true;
|
||||
group.version = 1234;
|
||||
yield group.save();
|
||||
it("should copy an item with an attachment to a group", function* () {
|
||||
var group = yield getGroup();
|
||||
|
||||
var item = yield createDataObject('item', false, {
|
||||
skipSelect: true
|
||||
});
|
||||
var item = yield createDataObject('item', false, { skipSelect: true });
|
||||
var file = getTestDataDirectory();
|
||||
file.append('test.png');
|
||||
yield Zotero.Attachments.importFromFile({
|
||||
var attachment = yield Zotero.Attachments.importFromFile({
|
||||
file: file,
|
||||
parentItemID: item.id
|
||||
});
|
||||
|
||||
var row = collectionsView.getRowIndexByID("L" + group.libraryID);
|
||||
// Hack to unload relations to test proper loading
|
||||
//
|
||||
// Probably need a better method for this
|
||||
item._loaded.relations = false;
|
||||
attachment._loaded.relations = false;
|
||||
|
||||
// Simulate a drag and drop
|
||||
var stub = sinon.stub(Zotero.DragDrop, "getDragTarget");
|
||||
stub.returns(collectionsView.getRow(row));
|
||||
collectionsView.drop(row, 0, {
|
||||
dropEffect: 'copy',
|
||||
effectAllowed: 'copy',
|
||||
mozSourceNode: win.document.getElementById('zotero-items-tree'),
|
||||
types: {
|
||||
contains: function (type) {
|
||||
return type == 'zotero/item';
|
||||
}
|
||||
},
|
||||
getData: function (type) {
|
||||
if (type == 'zotero/item') {
|
||||
return "" + item.id;
|
||||
}
|
||||
}
|
||||
});
|
||||
var ids = yield drop("L" + group.libraryID, [item.id]);
|
||||
|
||||
// Add observer to wait for collection add
|
||||
var ids = yield waitForItemEvent("add");
|
||||
|
||||
stub.restore();
|
||||
yield collectionsView.selectLibrary(group.libraryID);
|
||||
yield waitForItemsLoad(win);
|
||||
|
||||
var itemsView = win.ZoteroPane.itemsView
|
||||
assert.equal(itemsView.rowCount, 1);
|
||||
var treeRow = itemsView.getRow(0);
|
||||
assert.equal(treeRow.ref.libraryID, group.libraryID);
|
||||
assert.equal(treeRow.ref.id, ids[0]);
|
||||
|
||||
yield group.erase();
|
||||
// New item should link back to original
|
||||
var linked = yield item.getLinkedItem(group.libraryID);
|
||||
assert.equal(linked.id, treeRow.ref.id);
|
||||
})
|
||||
|
||||
it("should not copy an item or its attachment to a group twice", function* () {
|
||||
var group = yield getGroup();
|
||||
|
||||
var itemTitle = Zotero.Utilities.randomString();
|
||||
var item = yield createDataObject('item', false, { skipSelect: true });
|
||||
var file = getTestDataDirectory();
|
||||
file.append('test.png');
|
||||
var attachment = yield Zotero.Attachments.importFromFile({
|
||||
file: file,
|
||||
parentItemID: item.id
|
||||
});
|
||||
var attachmentTitle = Zotero.Utilities.randomString();
|
||||
attachment.setField('title', attachmentTitle);
|
||||
yield attachment.save();
|
||||
|
||||
var ids = yield drop("L" + group.libraryID, [item.id]);
|
||||
assert.isFalse(yield canDrop("L" + group.libraryID, [item.id]));
|
||||
})
|
||||
|
||||
it("should remove a linked, trashed item in a group from the trash and collections", function* () {
|
||||
var group = yield getGroup();
|
||||
var collection = yield createDataObject('collection', { libraryID: group.libraryID });
|
||||
|
||||
var item = yield createDataObject('item', false, { skipSelect: true });
|
||||
var ids = yield drop("L" + group.libraryID, [item.id]);
|
||||
|
||||
var droppedItem = yield item.getLinkedItem(group.libraryID);
|
||||
droppedItem.setCollections([collection.id]);
|
||||
droppedItem.deleted = true;
|
||||
yield droppedItem.save();
|
||||
|
||||
// Add observer to wait for collection add
|
||||
var deferred = Zotero.Promise.defer();
|
||||
var observerID = Zotero.Notifier.registerObserver({
|
||||
notify: function (event, type, ids) {
|
||||
if (event == 'refresh' && type == 'trash' && ids[0] == group.libraryID) {
|
||||
setTimeout(function () {
|
||||
deferred.resolve();
|
||||
});
|
||||
}
|
||||
}
|
||||
}, 'trash', 'test');
|
||||
var ids = yield drop("L" + group.libraryID, [item.id], deferred.promise);
|
||||
Zotero.Notifier.unregisterObserver(observerID);
|
||||
|
||||
assert.isFalse(droppedItem.deleted);
|
||||
// Should be removed from collections when removed from trash
|
||||
assert.lengthOf(droppedItem.getCollections(), 0);
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -171,4 +171,111 @@ describe("Zotero.DataObject", function() {
|
|||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("Relations", function () {
|
||||
var types = ['collection', 'item'];
|
||||
|
||||
function makeObjectURI(objectType) {
|
||||
var objectTypePlural = Zotero.DataObjectUtilities.getObjectTypePlural(objectType);
|
||||
return 'http://zotero.org/groups/1/' + objectTypePlural + '/'
|
||||
+ Zotero.Utilities.generateObjectKey();
|
||||
}
|
||||
|
||||
describe("#addRelation()", function () {
|
||||
it("should add a relation to an object", function* () {
|
||||
for (let type of types) {
|
||||
let predicate = 'owl:sameAs';
|
||||
let object = makeObjectURI(type);
|
||||
let obj = createUnsavedDataObject(type);
|
||||
obj.addRelation(predicate, object);
|
||||
yield obj.saveTx();
|
||||
var relations = obj.getRelations();
|
||||
assert.property(relations, predicate);
|
||||
assert.include(relations[predicate], object);
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("#removeRelation()", function () {
|
||||
it("should remove a relation from an object", function* () {
|
||||
for (let type of types) {
|
||||
let predicate = 'owl:sameAs';
|
||||
let object = makeObjectURI(type);
|
||||
let obj = createUnsavedDataObject(type);
|
||||
obj.addRelation(predicate, object);
|
||||
yield obj.saveTx();
|
||||
|
||||
obj.removeRelation(predicate, object);
|
||||
yield obj.saveTx();
|
||||
|
||||
assert.lengthOf(Object.keys(obj.getRelations()), 0);
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("#hasRelation()", function () {
|
||||
it("should return true if an object has a given relation", function* () {
|
||||
for (let type of types) {
|
||||
let predicate = 'owl:sameAs';
|
||||
let object = makeObjectURI(type);
|
||||
let obj = createUnsavedDataObject(type);
|
||||
obj.addRelation(predicate, object);
|
||||
yield obj.saveTx();
|
||||
assert.ok(obj.hasRelation(predicate, object));
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("#_getLinkedObject()", function () {
|
||||
it("should return a linked object in another library", function* () {
|
||||
var group = yield getGroup();
|
||||
var item1 = yield createDataObject('item');
|
||||
var item2 = yield createDataObject('item', { libraryID: group.libraryID });
|
||||
var item2URI = Zotero.URI.getItemURI(item2);
|
||||
|
||||
yield item2.addLinkedItem(item1);
|
||||
var linkedItem = yield item1.getLinkedItem(item2.libraryID);
|
||||
assert.equal(linkedItem.id, item2.id);
|
||||
})
|
||||
|
||||
it("shouldn't return reverse linked objects by default", function* () {
|
||||
var group = yield getGroup();
|
||||
var item1 = yield createDataObject('item');
|
||||
var item1URI = Zotero.URI.getItemURI(item1);
|
||||
var item2 = yield createDataObject('item', { libraryID: group.libraryID });
|
||||
|
||||
yield item2.addLinkedItem(item1);
|
||||
var linkedItem = yield item2.getLinkedItem(item1.libraryID);
|
||||
assert.isFalse(linkedItem);
|
||||
})
|
||||
|
||||
it("should return reverse linked objects with bidirectional flag", function* () {
|
||||
var group = yield getGroup();
|
||||
var item1 = yield createDataObject('item');
|
||||
var item1URI = Zotero.URI.getItemURI(item1);
|
||||
var item2 = yield createDataObject('item', { libraryID: group.libraryID });
|
||||
|
||||
yield item2.addLinkedItem(item1);
|
||||
var linkedItem = yield item2.getLinkedItem(item1.libraryID, true);
|
||||
assert.equal(linkedItem.id, item1.id);
|
||||
})
|
||||
})
|
||||
|
||||
describe("#_addLinkedObject()", function () {
|
||||
it("should add an owl:sameAs relation", function* () {
|
||||
var group = yield getGroup();
|
||||
var item1 = yield createDataObject('item');
|
||||
var dateModified = item1.getField('dateModified');
|
||||
var item2 = yield createDataObject('item', { libraryID: group.libraryID });
|
||||
var item2URI = Zotero.URI.getItemURI(item2);
|
||||
|
||||
yield item2.addLinkedItem(item1);
|
||||
var preds = item1.getRelationsByPredicate(Zotero.Relations.linkedObjectPredicate);
|
||||
assert.include(preds, item2URI);
|
||||
|
||||
// Make sure Date Modified hasn't changed
|
||||
assert.equal(item1.getField('dateModified'), dateModified);
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -588,6 +588,37 @@ describe("Zotero.Item", function () {
|
|||
})
|
||||
})
|
||||
|
||||
//
|
||||
// Relations and related items
|
||||
//
|
||||
describe("#addRelatedItem", function () {
|
||||
it("#should add a dc:relation relation to an item", function* () {
|
||||
var item1 = yield createDataObject('item');
|
||||
var item2 = yield createDataObject('item');
|
||||
item1.addRelatedItem(item2);
|
||||
yield item1.save();
|
||||
|
||||
var rels = item1.getRelationsByPredicate(Zotero.Relations.relatedItemPredicate);
|
||||
assert.lengthOf(rels, 1);
|
||||
assert.equal(rels[0], Zotero.URI.getItemURI(item2));
|
||||
})
|
||||
|
||||
it("#should throw an error for a relation in a different library", function* () {
|
||||
var group = yield getGroup();
|
||||
var item1 = yield createDataObject('item');
|
||||
var item2 = yield createDataObject('item', { libraryID: group.libraryID });
|
||||
try {
|
||||
item1.addRelatedItem(item2)
|
||||
}
|
||||
catch (e) {
|
||||
assert.ok(e);
|
||||
assert.equal(e.message, "Cannot relate item to an item in a different library");
|
||||
return;
|
||||
}
|
||||
assert.fail("addRelatedItem() allowed for an item in a different library");
|
||||
})
|
||||
})
|
||||
|
||||
describe("#clone()", function () {
|
||||
// TODO: Expand to other data
|
||||
it("should copy creators", function* () {
|
||||
|
|
|
@ -13,6 +13,78 @@ describe("Zotero.Items", function () {
|
|||
win.close();
|
||||
})
|
||||
|
||||
|
||||
describe("#merge()", function () {
|
||||
it("should merge two items", function* () {
|
||||
var item1 = yield createDataObject('item');
|
||||
var item2 = yield createDataObject('item');
|
||||
var item2URI = Zotero.URI.getItemURI(item2);
|
||||
|
||||
yield Zotero.Items.merge(item1, [item2]);
|
||||
|
||||
assert.isFalse(item1.deleted);
|
||||
assert.isTrue(item2.deleted);
|
||||
|
||||
// Check for merge-tracking relation
|
||||
var rels = item1.getRelationsByPredicate(Zotero.Relations.replacedItemPredicate);
|
||||
assert.lengthOf(rels, 1);
|
||||
assert.equal(rels[0], item2URI);
|
||||
})
|
||||
|
||||
it("should move merge-tracking relation from replaced item to master", function* () {
|
||||
var item1 = yield createDataObject('item');
|
||||
var item2 = yield createDataObject('item');
|
||||
var item2URI = Zotero.URI.getItemURI(item2);
|
||||
var item3 = yield createDataObject('item');
|
||||
var item3URI = Zotero.URI.getItemURI(item3);
|
||||
|
||||
yield Zotero.Items.merge(item2, [item3]);
|
||||
yield Zotero.Items.merge(item1, [item2]);
|
||||
|
||||
// Check for merge-tracking relation from 1 to 3
|
||||
var rels = item1.getRelationsByPredicate(Zotero.Relations.replacedItemPredicate);
|
||||
assert.lengthOf(rels, 2);
|
||||
assert.sameMembers(rels, [item2URI, item3URI]);
|
||||
})
|
||||
|
||||
it("should update relations pointing to replaced item to point to master", function* () {
|
||||
var item1 = yield createDataObject('item');
|
||||
var item1URI = Zotero.URI.getItemURI(item1);
|
||||
var item2 = yield createDataObject('item');
|
||||
var item2URI = Zotero.URI.getItemURI(item2);
|
||||
var item3 = createUnsavedDataObject('item');
|
||||
var predicate = Zotero.Relations.relatedItemPredicate;
|
||||
item3.addRelation(predicate, item2URI);
|
||||
yield item3.saveTx();
|
||||
|
||||
yield Zotero.Items.merge(item1, [item2]);
|
||||
|
||||
// Check for related-item relation from 3 to 1
|
||||
var rels = item3.getRelationsByPredicate(predicate);
|
||||
assert.deepEqual(rels, [item1URI]);
|
||||
})
|
||||
|
||||
it("should not update relations pointing to replaced item in other libraries", function* () {
|
||||
var group1 = yield createGroup();
|
||||
var group2 = yield createGroup();
|
||||
|
||||
var item1 = yield createDataObject('item', { libraryID: group1.libraryID });
|
||||
var item1URI = Zotero.URI.getItemURI(item1);
|
||||
var item2 = yield createDataObject('item', { libraryID: group1.libraryID });
|
||||
var item2URI = Zotero.URI.getItemURI(item2);
|
||||
var item3 = createUnsavedDataObject('item', { libraryID: group2.libraryID });
|
||||
var predicate = Zotero.Relations.linkedObjectPredicate;
|
||||
item3.addRelation(predicate, item2URI);
|
||||
yield item3.saveTx();
|
||||
|
||||
yield Zotero.Items.merge(item1, [item2]);
|
||||
|
||||
// Check for related-item relation from 3 to 2
|
||||
var rels = item3.getRelationsByPredicate(predicate);
|
||||
assert.deepEqual(rels, [item2URI]);
|
||||
})
|
||||
})
|
||||
|
||||
describe("#emptyTrash()", function () {
|
||||
it("should delete items in the trash", function* () {
|
||||
var item1 = createUnsavedDataObject('item');
|
||||
|
|
103
test/tests/relatedboxTest.js
Normal file
103
test/tests/relatedboxTest.js
Normal file
|
@ -0,0 +1,103 @@
|
|||
"use strict";
|
||||
|
||||
describe("Related Box", function () {
|
||||
var win, doc, itemsView;
|
||||
|
||||
before(function* () {
|
||||
win = yield loadZoteroPane();
|
||||
doc = win.document;
|
||||
itemsView = win.ZoteroPane.itemsView;
|
||||
});
|
||||
after(function () {
|
||||
win.close();
|
||||
})
|
||||
|
||||
describe("Add button", function () {
|
||||
it("should add a related item", function* () {
|
||||
var item1 = yield createDataObject('item');
|
||||
var item2 = yield createDataObject('item');
|
||||
|
||||
// Select the Related pane
|
||||
var tabbox = doc.getElementById('zotero-view-tabbox');
|
||||
tabbox.selectedIndex = 3;
|
||||
var relatedbox = doc.getElementById('zotero-editpane-related');
|
||||
assert.lengthOf(relatedbox.id('relatedRows').childNodes, 0);
|
||||
|
||||
// Click the Add button to open the Select Items dialog
|
||||
setTimeout(function () {
|
||||
relatedbox.id('addButton').click();
|
||||
});
|
||||
var selectWin = yield waitForWindow('chrome://zotero/content/selectItemsDialog.xul');
|
||||
// wrappedJSObject isn't working on zotero-collections-tree for some reason, so
|
||||
// just wait for the items tree to be created and select it directly
|
||||
do {
|
||||
var view = selectWin.document.getElementById('zotero-items-tree').view.wrappedJSObject;
|
||||
yield Zotero.Promise.delay(50);
|
||||
}
|
||||
while (!view);
|
||||
var deferred = Zotero.Promise.defer();
|
||||
view.addEventListener('load', () => deferred.resolve());
|
||||
yield deferred.promise;
|
||||
|
||||
// Select the other item
|
||||
for (let i = 0; i < view.rowCount; i++) {
|
||||
if (view.getRow(i).ref.id == item1.id) {
|
||||
view.selection.select(i);
|
||||
}
|
||||
}
|
||||
selectWin.document.documentElement.acceptDialog();
|
||||
|
||||
// Wait for relations list to populate
|
||||
do {
|
||||
yield Zotero.Promise.delay(50);
|
||||
}
|
||||
while (!relatedbox.id('relatedRows').childNodes.length);
|
||||
|
||||
assert.lengthOf(relatedbox.id('relatedRows').childNodes, 1);
|
||||
|
||||
var items = item1.relatedItems;
|
||||
assert.lengthOf(items, 1);
|
||||
assert.equal(items[0], item2.key);
|
||||
|
||||
// Relation should be assigned bidirectionally
|
||||
var items = item2.relatedItems;
|
||||
assert.lengthOf(items, 1);
|
||||
assert.equal(items[0], item1.key);
|
||||
})
|
||||
})
|
||||
|
||||
describe("Remove button", function () {
|
||||
it("should remove a related item", function* () {
|
||||
var item1 = yield createDataObject('item');
|
||||
var item2 = yield createDataObject('item');
|
||||
|
||||
yield item1.loadRelations();
|
||||
item1.addRelatedItem(item2);
|
||||
yield item1.save();
|
||||
yield item2.loadRelations();
|
||||
item2.addRelatedItem(item1);
|
||||
yield item2.save();
|
||||
|
||||
// Select the Related pane
|
||||
var tabbox = doc.getElementById('zotero-view-tabbox');
|
||||
tabbox.selectedIndex = 3;
|
||||
var relatedbox = doc.getElementById('zotero-editpane-related');
|
||||
|
||||
// Wait for relations list to populate
|
||||
do {
|
||||
yield Zotero.Promise.delay(50);
|
||||
}
|
||||
while (!relatedbox.id('relatedRows').childNodes.length);
|
||||
|
||||
doc.getAnonymousNodes(relatedbox)[0]
|
||||
.getElementsByAttribute('value', '-')[0]
|
||||
.click();
|
||||
|
||||
// Wait for relations list to clear
|
||||
do {
|
||||
yield Zotero.Promise.delay(50);
|
||||
}
|
||||
while (relatedbox.id('relatedRows').childNodes.length);
|
||||
})
|
||||
})
|
||||
})
|
24
test/tests/relationsTest.js
Normal file
24
test/tests/relationsTest.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
"use strict";
|
||||
|
||||
describe("Zotero.Relations", function () {
|
||||
describe("#getByPredicateAndObject()", function () {
|
||||
it("should return items matching predicate and object", function* () {
|
||||
var item = createUnsavedDataObject('item');
|
||||
item.setRelations({
|
||||
"dc:relation": [
|
||||
"http://zotero.org/users/1/items/SHREREMS"
|
||||
],
|
||||
"owl:sameAs": [
|
||||
"http://zotero.org/groups/1/items/SRRMGSRM",
|
||||
"http://zotero.org/groups/1/items/GSMRRSSM"
|
||||
]
|
||||
})
|
||||
yield item.saveTx();
|
||||
var objects = yield Zotero.Relations.getByPredicateAndObject(
|
||||
'item', 'owl:sameAs', 'http://zotero.org/groups/1/items/SRRMGSRM'
|
||||
);
|
||||
assert.lengthOf(objects, 1);
|
||||
assert.equal(objects[0], item);
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Reference in a new issue