2015-05-05 07:17:12 +00:00
|
|
|
"use strict";
|
|
|
|
|
|
|
|
describe("Zotero.DataObject", function() {
|
|
|
|
var types = ['collection', 'item', 'search'];
|
|
|
|
|
2016-04-10 23:46:10 +00:00
|
|
|
describe("#library", function () {
|
|
|
|
it("should return a Zotero.Library", function* () {
|
|
|
|
var item = yield createDataObject('item');
|
|
|
|
assert.equal(item.library, Zotero.Libraries.userLibrary);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe("#libraryID", function () {
|
|
|
|
it("should return a libraryID", function* () {
|
|
|
|
var item = yield createDataObject('item');
|
|
|
|
assert.isNumber(item.libraryID);
|
|
|
|
assert.equal(item.libraryID, Zotero.Libraries.userLibraryID);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2015-07-31 08:03:09 +00:00
|
|
|
describe("#key", function () {
|
|
|
|
it("shouldn't update .loaded on get if unset", function* () {
|
|
|
|
for (let type of types) {
|
2015-09-17 01:19:33 +00:00
|
|
|
let param;
|
2015-07-31 08:03:09 +00:00
|
|
|
if (type == 'item') {
|
2015-09-17 01:19:33 +00:00
|
|
|
param = 'book';
|
2015-07-31 08:03:09 +00:00
|
|
|
}
|
|
|
|
let obj = new Zotero[Zotero.Utilities.capitalize(type)](param);
|
|
|
|
obj.libraryID = Zotero.Libraries.userLibraryID;
|
2015-09-17 01:19:33 +00:00
|
|
|
assert.isNull(obj.key, 'key is null for ' + type);
|
|
|
|
assert.isFalse(obj._loaded.primaryData, 'primary data not loaded for ' + type);
|
2015-07-31 08:03:09 +00:00
|
|
|
obj.key = Zotero.DataObjectUtilities.generateKey();
|
|
|
|
}
|
2015-05-05 18:39:24 +00:00
|
|
|
})
|
|
|
|
})
|
|
|
|
|
2015-05-20 23:28:35 +00:00
|
|
|
describe("#version", function () {
|
|
|
|
it("should be set to 0 after creating object", function* () {
|
|
|
|
for (let type of types) {
|
|
|
|
let obj = yield createDataObject(type);
|
|
|
|
assert.equal(obj.version, 0);
|
|
|
|
yield obj.eraseTx();
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
it("should be set after creating object", function* () {
|
|
|
|
for (let type of types) {
|
|
|
|
let obj = yield createDataObject(type, { version: 1234 });
|
|
|
|
assert.equal(obj.version, 1234, type + " version mismatch");
|
|
|
|
yield obj.eraseTx();
|
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
2015-05-13 23:30:22 +00:00
|
|
|
describe("#synced", function () {
|
2015-07-19 21:17:32 +00:00
|
|
|
it("should be set to false after creating object", function* () {
|
|
|
|
for (let type of types) {
|
|
|
|
var obj = createUnsavedDataObject(type);
|
|
|
|
var id = yield obj.saveTx();
|
|
|
|
assert.isFalse(obj.synced);
|
|
|
|
yield obj.eraseTx();
|
|
|
|
}
|
2015-05-13 23:30:22 +00:00
|
|
|
});
|
|
|
|
|
2015-07-19 21:17:32 +00:00
|
|
|
it("should be set to false after modifying object", function* () {
|
|
|
|
for (let type of types) {
|
|
|
|
var obj = createUnsavedDataObject(type);
|
|
|
|
var id = yield obj.saveTx();
|
|
|
|
|
|
|
|
obj.synced = true;
|
|
|
|
yield obj.saveTx();
|
|
|
|
|
|
|
|
if (type == 'item') {
|
|
|
|
obj.setField('title', Zotero.Utilities.randomString());
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
obj.name = Zotero.Utilities.randomString();
|
|
|
|
}
|
|
|
|
yield obj.saveTx();
|
|
|
|
assert.isFalse(obj.synced);
|
|
|
|
|
|
|
|
yield obj.eraseTx();
|
|
|
|
}
|
2015-05-13 23:30:22 +00:00
|
|
|
});
|
|
|
|
|
2015-07-19 21:17:32 +00:00
|
|
|
it("should be changed to true explicitly with no other changes", function* () {
|
|
|
|
for (let type of types) {
|
|
|
|
var obj = createUnsavedDataObject(type);
|
|
|
|
var id = yield obj.saveTx();
|
|
|
|
|
|
|
|
obj.synced = true;
|
|
|
|
yield obj.saveTx();
|
|
|
|
assert.isTrue(obj.synced);
|
|
|
|
|
|
|
|
yield obj.eraseTx();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
it("should be changed to true explicitly with other field changes", function* () {
|
|
|
|
for (let type of types) {
|
|
|
|
var obj = createUnsavedDataObject(type);
|
|
|
|
var id = yield obj.saveTx();
|
|
|
|
|
|
|
|
if (type == 'item') {
|
|
|
|
obj.setField('title', Zotero.Utilities.randomString());
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
obj.name = Zotero.Utilities.randomString();
|
|
|
|
}
|
|
|
|
obj.synced = true;
|
|
|
|
yield obj.saveTx();
|
|
|
|
assert.isTrue(obj.synced);
|
|
|
|
|
|
|
|
yield obj.eraseTx();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
it("should remain at true if set explicitly", function* () {
|
|
|
|
for (let type of types) {
|
|
|
|
var obj = createUnsavedDataObject(type);
|
|
|
|
obj.synced = true;
|
|
|
|
var id = yield obj.saveTx();
|
|
|
|
assert.isTrue(obj.synced);
|
|
|
|
|
|
|
|
if (type == 'item') {
|
|
|
|
obj.setField('title', 'test');
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
obj.name = Zotero.Utilities.randomString();
|
|
|
|
}
|
|
|
|
obj.synced = true;
|
|
|
|
yield obj.saveTx();
|
|
|
|
assert.isTrue(obj.synced);
|
|
|
|
|
|
|
|
yield obj.eraseTx();
|
|
|
|
}
|
2015-05-13 23:30:22 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it("should be unchanged if skipSyncedUpdate passed", function* () {
|
2015-07-19 21:17:32 +00:00
|
|
|
for (let type of types) {
|
|
|
|
var obj = createUnsavedDataObject(type);
|
|
|
|
var id = yield obj.saveTx();
|
|
|
|
|
2020-06-21 06:18:06 +00:00
|
|
|
obj.synced = true;
|
2015-07-19 21:17:32 +00:00
|
|
|
yield obj.saveTx();
|
|
|
|
|
|
|
|
if (type == 'item') {
|
|
|
|
obj.setField('title', Zotero.Utilities.randomString());
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
obj.name = Zotero.Utilities.randomString();
|
|
|
|
}
|
|
|
|
yield obj.saveTx({
|
|
|
|
skipSyncedUpdate: true
|
|
|
|
});
|
|
|
|
assert.ok(obj.synced);
|
|
|
|
|
|
|
|
yield obj.eraseTx();
|
|
|
|
}
|
2015-05-13 23:30:22 +00:00
|
|
|
});
|
|
|
|
})
|
|
|
|
|
2021-01-13 05:40:13 +00:00
|
|
|
|
|
|
|
describe("#deleted", function () {
|
|
|
|
it("should set trash status", async function () {
|
|
|
|
for (let type of types) {
|
|
|
|
let plural = Zotero.DataObjectUtilities.getObjectTypePlural(type)
|
|
|
|
let pluralClass = Zotero[Zotero.Utilities.capitalize(plural)];
|
|
|
|
|
|
|
|
// Set to true
|
|
|
|
var obj = await createDataObject(type);
|
|
|
|
assert.isFalse(obj.deleted, type);
|
|
|
|
obj.deleted = true;
|
|
|
|
// Sanity check for itemsTest#trash()
|
|
|
|
if (type == 'item') {
|
|
|
|
assert.isTrue(obj._changedData.deleted, type);
|
|
|
|
}
|
|
|
|
await obj.saveTx();
|
|
|
|
var id = obj.id;
|
|
|
|
await pluralClass.reload(id, false, true);
|
|
|
|
assert.isTrue(obj.deleted, type);
|
|
|
|
|
|
|
|
// Set to false
|
|
|
|
obj.deleted = false;
|
|
|
|
await obj.saveTx();
|
|
|
|
await pluralClass.reload(id, false, true);
|
|
|
|
assert.isFalse(obj.deleted, type);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2015-07-31 08:03:09 +00:00
|
|
|
describe("#loadPrimaryData()", function () {
|
|
|
|
it("should load unloaded primary data if partially set", function* () {
|
|
|
|
var objs = {};
|
|
|
|
for (let type of types) {
|
|
|
|
let obj = createUnsavedDataObject(type);
|
|
|
|
yield obj.save({
|
|
|
|
skipCache: true
|
|
|
|
});
|
|
|
|
objs[type] = {
|
|
|
|
key: obj.key,
|
|
|
|
version: obj.version
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let type of types) {
|
|
|
|
let obj = new Zotero[Zotero.Utilities.capitalize(type)];
|
|
|
|
obj.libraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
obj.key = objs[type].key;
|
|
|
|
yield obj.loadPrimaryData();
|
|
|
|
assert.equal(obj.version, objs[type].version);
|
|
|
|
}
|
2020-06-23 08:59:10 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it("shouldn't overwrite item type set in constructor", async function () {
|
|
|
|
var item = new Zotero.Item('book');
|
|
|
|
item.libraryID = Zotero.Libraries.userLibraryID;
|
|
|
|
item.key = Zotero.DataObjectUtilities.generateKey();
|
|
|
|
await item.loadPrimaryData();
|
|
|
|
var saved = await item.saveTx();
|
|
|
|
assert.ok(saved);
|
|
|
|
});
|
2015-07-31 08:03:09 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
describe("#loadAllData()", function () {
|
|
|
|
it("should load data on a regular item", function* () {
|
|
|
|
var item = new Zotero.Item('book');
|
|
|
|
var id = yield item.saveTx();
|
|
|
|
yield item.loadAllData();
|
2020-12-28 04:41:12 +00:00
|
|
|
assert.throws(item.getNote.bind(item), 'getNote() can only be called on notes and attachments');
|
2015-07-31 08:03:09 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
it("should load data on an attachment item", function* () {
|
|
|
|
var item = new Zotero.Item('attachment');
|
|
|
|
var id = yield item.saveTx();
|
|
|
|
yield item.loadAllData();
|
2020-08-25 18:22:23 +00:00
|
|
|
assert.equal(item.note, '');
|
2015-07-31 08:03:09 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
it("should load data on a note item", function* () {
|
|
|
|
var item = new Zotero.Item('note');
|
|
|
|
var id = yield item.saveTx();
|
|
|
|
yield item.loadAllData();
|
2020-08-25 18:22:23 +00:00
|
|
|
assert.equal(item.note, '');
|
2015-07-31 08:03:09 +00:00
|
|
|
})
|
|
|
|
})
|
|
|
|
|
2015-08-02 09:26:02 +00:00
|
|
|
|
|
|
|
describe("#hasChanged()", function () {
|
|
|
|
it("should return false if 'synced' was set but unchanged and nothing else changed", function* () {
|
|
|
|
for (let type of types) {
|
|
|
|
// True
|
|
|
|
var obj = createUnsavedDataObject(type);
|
|
|
|
obj.synced = true;
|
|
|
|
var id = yield obj.saveTx();
|
|
|
|
assert.isTrue(obj.synced);
|
|
|
|
|
|
|
|
obj.synced = true;
|
|
|
|
assert.isFalse(obj.hasChanged(), type + " shouldn't be changed");
|
|
|
|
|
|
|
|
// False
|
|
|
|
var obj = createUnsavedDataObject(type);
|
|
|
|
obj.synced = false;
|
|
|
|
var id = yield obj.saveTx();
|
|
|
|
assert.isFalse(obj.synced);
|
|
|
|
obj.synced = false;
|
|
|
|
assert.isFalse(obj.hasChanged(), type + " shouldn't be changed");
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
it("should return true if 'synced' was set but unchanged and another primary field changed", function* () {
|
|
|
|
for (let type of types) {
|
|
|
|
let obj = createUnsavedDataObject(type);
|
|
|
|
obj.synced = true;
|
|
|
|
yield obj.saveTx();
|
|
|
|
|
|
|
|
obj.synced = true;
|
|
|
|
obj.version = 1234;
|
|
|
|
assert.isTrue(obj.hasChanged());
|
|
|
|
}
|
|
|
|
})
|
2017-07-16 22:15:12 +00:00
|
|
|
});
|
|
|
|
|
2015-08-02 09:26:02 +00:00
|
|
|
|
2015-05-05 07:17:12 +00:00
|
|
|
describe("#save()", function () {
|
|
|
|
it("should add new identifiers to cache", function* () {
|
2015-12-08 21:38:30 +00:00
|
|
|
for (let type of types) {
|
|
|
|
let objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type);
|
|
|
|
let obj = createUnsavedDataObject(type);
|
|
|
|
let id = yield obj.saveTx();
|
|
|
|
let { libraryID, key } = objectsClass.getLibraryAndKeyFromID(id);
|
|
|
|
assert.typeOf(key, 'string');
|
|
|
|
assert.equal(objectsClass.getIDFromLibraryAndKey(libraryID, key), id);
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
it("should reset changed state on objects", function* () {
|
|
|
|
for (let type of types) {
|
|
|
|
let objectsClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(type);
|
|
|
|
let obj = createUnsavedDataObject(type);
|
|
|
|
yield obj.saveTx();
|
|
|
|
assert.isFalse(obj.hasChanged());
|
|
|
|
}
|
2015-05-05 07:17:12 +00:00
|
|
|
})
|
2017-05-22 21:29:56 +00:00
|
|
|
|
2017-07-16 22:15:12 +00:00
|
|
|
it("should handle additional tag change in the middle of a save", function* () {
|
|
|
|
var item = yield createDataObject('item');
|
|
|
|
item.setTags(['a']);
|
|
|
|
|
|
|
|
var deferred = new Zotero.Promise.defer();
|
|
|
|
var origFunc = Zotero.Notifier.queue.bind(Zotero.Notifier);
|
|
|
|
sinon.stub(Zotero.Notifier, "queue").callsFake(function (event, type, ids, extraData) {
|
|
|
|
// Add a new tag after the first one has been added to the DB and before the save is
|
|
|
|
// finished. The changed state should've cleared before saving to the DB the first
|
|
|
|
// time, so the second setTags() should mark the item as changed and allow the new tag
|
|
|
|
// to be saved in the second saveTx().
|
|
|
|
if (event == 'add' && type == 'item-tag') {
|
|
|
|
item.setTags(['a', 'b']);
|
|
|
|
Zotero.Notifier.queue.restore();
|
|
|
|
deferred.resolve(item.saveTx());
|
|
|
|
}
|
|
|
|
origFunc(...arguments);
|
|
|
|
});
|
|
|
|
|
|
|
|
yield Zotero.Promise.all([item.saveTx(), deferred.promise]);
|
|
|
|
assert.sameMembers(item.getTags().map(o => o.tag), ['a', 'b']);
|
|
|
|
var tags = yield Zotero.DB.columnQueryAsync(
|
|
|
|
"SELECT name FROM tags JOIN itemTags USING (tagID) WHERE itemID=?", item.id
|
|
|
|
);
|
|
|
|
assert.sameMembers(tags, ['a', 'b']);
|
|
|
|
});
|
|
|
|
|
2017-05-22 21:29:56 +00:00
|
|
|
describe("Edit Check", function () {
|
|
|
|
var group;
|
|
|
|
|
|
|
|
before(function* () {
|
|
|
|
group = yield createGroup({
|
|
|
|
editable: false
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it("should disallow saving to read-only libraries", function* () {
|
|
|
|
let item = createUnsavedDataObject('item', { libraryID: group.libraryID });
|
|
|
|
var e = yield getPromiseError(item.saveTx());
|
|
|
|
assert.ok(e);
|
2022-01-31 09:26:46 +00:00
|
|
|
assert.include(e.message, "Cannot edit item");
|
2017-05-22 21:29:56 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it("should allow saving if skipEditCheck is passed", function* () {
|
|
|
|
let item = createUnsavedDataObject('item', { libraryID: group.libraryID });
|
|
|
|
var e = yield getPromiseError(item.saveTx({
|
|
|
|
skipEditCheck: true
|
|
|
|
}));
|
|
|
|
assert.isFalse(e);
|
|
|
|
});
|
|
|
|
|
|
|
|
it("should allow saving if skipAll is passed", function* () {
|
|
|
|
let item = createUnsavedDataObject('item', { libraryID: group.libraryID });
|
|
|
|
var e = yield getPromiseError(item.saveTx({
|
|
|
|
skipAll: true
|
|
|
|
}));
|
|
|
|
assert.isFalse(e);
|
|
|
|
});
|
|
|
|
});
|
2015-05-05 07:17:12 +00:00
|
|
|
})
|
2015-05-22 01:56:04 +00:00
|
|
|
|
2015-06-02 07:28:46 +00:00
|
|
|
describe("#erase()", function () {
|
|
|
|
it("shouldn't trigger notifier if skipNotifier is passed", function* () {
|
|
|
|
let observerIDs = [];
|
|
|
|
let promises = [];
|
|
|
|
for (let type of types) {
|
|
|
|
let obj = yield createDataObject(type);
|
|
|
|
// For items, test automatic child item deletion
|
|
|
|
if (type == 'item') {
|
|
|
|
yield createDataObject(type, { itemType: 'note', parentID: obj.id });
|
|
|
|
}
|
|
|
|
|
|
|
|
let deferred = Zotero.Promise.defer();
|
|
|
|
promises.push(deferred.promise);
|
|
|
|
observerIDs.push(Zotero.Notifier.registerObserver(
|
|
|
|
{
|
|
|
|
notify: function (event) {
|
|
|
|
if (event == 'delete') {
|
|
|
|
deferred.reject("Notifier called for erase on " + type);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
type,
|
|
|
|
'test'
|
|
|
|
));
|
|
|
|
yield obj.eraseTx({
|
|
|
|
skipNotifier: true
|
|
|
|
});
|
|
|
|
}
|
|
|
|
yield Zotero.Promise.all(promises)
|
|
|
|
// Give notifier time to trigger
|
|
|
|
.timeout(100).catch(Zotero.Promise.TimeoutError, (e) => {})
|
|
|
|
|
|
|
|
for (let id of observerIDs) {
|
|
|
|
Zotero.Notifier.unregisterObserver(id);
|
|
|
|
}
|
|
|
|
})
|
2015-08-06 09:25:45 +00:00
|
|
|
|
|
|
|
it("should delete object versions from sync cache", function* () {
|
|
|
|
for (let type of types) {
|
|
|
|
let obj = yield createDataObject(type);
|
|
|
|
let libraryID = obj.libraryID;
|
|
|
|
let key = obj.key;
|
Deasyncification :back: :cry:
While trying to get translation and citing working with asynchronously
generated data, we realized that drag-and-drop support was going to
be...problematic. Firefox only supports synchronous methods for
providing drag data (unlike, it seems, the DataTransferItem interface
supported by Chrome), which means that we'd need to preload all relevant
data on item selection (bounded by export.quickCopy.dragLimit) and keep
the translate/cite methods synchronous (or maintain two separate
versions).
What we're trying instead is doing what I said in #518 we weren't going
to do: loading most object data on startup and leaving many more
functions synchronous. Essentially, this takes the various load*()
methods described in #518, moves them to startup, and makes them operate
on entire libraries rather than individual objects.
The obvious downside here (other than undoing much of the work of the
last many months) is that it increases startup time, potentially quite a
lot for larger libraries. On my laptop, with a 3,000-item library, this
adds about 3 seconds to startup time. I haven't yet tested with larger
libraries. But I'm hoping that we can optimize this further to reduce
that delay. Among other things, this is loading data for all libraries,
when it should be able to load data only for the library being viewed.
But this is also fundamentally just doing some SELECT queries and
storing the results, so it really shouldn't need to be that slow (though
performance may be bounded a bit here by XPCOM overhead).
If we can make this fast enough, it means that third-party plugins
should be able to remain much closer to their current designs. (Some
things, including saving, will still need to be made asynchronous.)
2016-03-07 21:05:51 +00:00
|
|
|
let json = obj.toJSON();
|
2015-08-06 09:25:45 +00:00
|
|
|
yield Zotero.Sync.Data.Local.saveCacheObjects(type, libraryID, [json]);
|
|
|
|
yield obj.eraseTx();
|
|
|
|
let versions = yield Zotero.Sync.Data.Local.getCacheObjectVersions(
|
|
|
|
type, libraryID, key
|
|
|
|
);
|
|
|
|
assert.lengthOf(versions, 0);
|
|
|
|
}
|
|
|
|
})
|
2015-06-02 07:28:46 +00:00
|
|
|
})
|
|
|
|
|
2015-05-22 01:56:04 +00:00
|
|
|
describe("#updateVersion()", function() {
|
|
|
|
it("should update the object version", function* () {
|
|
|
|
for (let type of types) {
|
|
|
|
let obj = yield createDataObject(type);
|
|
|
|
assert.equal(obj.version, 0);
|
|
|
|
|
|
|
|
yield obj.updateVersion(1234);
|
|
|
|
assert.equal(obj.version, 1234);
|
|
|
|
assert.isFalse(obj.hasChanged());
|
|
|
|
|
|
|
|
obj.synced = true;
|
|
|
|
assert.ok(obj.hasChanged());
|
|
|
|
yield obj.updateVersion(1235);
|
|
|
|
assert.equal(obj.version, 1235);
|
|
|
|
assert.ok(obj.hasChanged());
|
|
|
|
|
|
|
|
yield obj.eraseTx();
|
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
describe("#updateSynced()", function() {
|
|
|
|
it("should update the object sync status", function* () {
|
|
|
|
for (let type of types) {
|
|
|
|
let obj = yield createDataObject(type);
|
|
|
|
assert.isFalse(obj.synced);
|
|
|
|
|
|
|
|
yield obj.updateSynced(false);
|
|
|
|
assert.isFalse(obj.synced);
|
|
|
|
assert.isFalse(obj.hasChanged());
|
|
|
|
|
|
|
|
yield obj.updateSynced(true);
|
|
|
|
assert.ok(obj.synced);
|
|
|
|
assert.isFalse(obj.hasChanged());
|
|
|
|
|
|
|
|
obj.version = 1234;
|
|
|
|
assert.ok(obj.hasChanged());
|
|
|
|
yield obj.updateSynced(false);
|
|
|
|
assert.isFalse(obj.synced);
|
|
|
|
assert.ok(obj.hasChanged());
|
|
|
|
|
|
|
|
yield obj.eraseTx();
|
|
|
|
}
|
|
|
|
})
|
2017-02-27 08:11:09 +00:00
|
|
|
|
|
|
|
it("should clear changed status", function* () {
|
|
|
|
var item = createUnsavedDataObject('item');
|
|
|
|
item.synced = true;
|
|
|
|
yield item.saveTx();
|
|
|
|
|
|
|
|
// Only synced changed
|
|
|
|
item.synced = false;
|
|
|
|
assert.isTrue(item.hasChanged());
|
|
|
|
assert.isTrue(item._changed.primaryData.synced);
|
|
|
|
yield item.updateSynced(true);
|
|
|
|
assert.isFalse(item.hasChanged());
|
|
|
|
// Should clear primary data change object
|
|
|
|
assert.isUndefined(item._changed.primaryData);
|
|
|
|
|
|
|
|
// Another primary field also changed
|
|
|
|
item.setField('dateModified', '2017-02-27 12:34:56');
|
|
|
|
item.synced = false;
|
|
|
|
assert.isTrue(item.hasChanged());
|
|
|
|
assert.isTrue(item._changed.primaryData.synced);
|
|
|
|
yield item.updateSynced(true);
|
|
|
|
assert.isTrue(item.hasChanged());
|
|
|
|
// Should clear only 'synced' change status
|
|
|
|
assert.isUndefined(item._changed.primaryData.synced);
|
|
|
|
});
|
2015-05-22 01:56:04 +00:00
|
|
|
})
|
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.
2015-06-02 00:09:39 +00:00
|
|
|
|
|
|
|
describe("Relations", function () {
|
|
|
|
var types = ['collection', 'item'];
|
|
|
|
|
|
|
|
function makeObjectURI(objectType) {
|
|
|
|
var objectTypePlural = Zotero.DataObjectUtilities.getObjectTypePlural(objectType);
|
|
|
|
return 'http://zotero.org/groups/1/' + objectTypePlural + '/'
|
2021-07-26 10:14:56 +00:00
|
|
|
+ Zotero.Utilities.generateObjectKey();
|
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.
2015-06-02 00:09:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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));
|
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
2017-09-22 05:01:52 +00:00
|
|
|
describe("#setRelations()", function () {
|
|
|
|
it("shouldn't allow invalid 'relations' predicates", function* () {
|
|
|
|
var item = new Zotero.Item("book");
|
|
|
|
assert.throws(() => {
|
|
|
|
item.setRelations({
|
|
|
|
"0": ["http://example.com/foo"]
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
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.
2015-06-02 00:09:39 +00:00
|
|
|
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);
|
2017-03-03 21:33:48 +00:00
|
|
|
var linkedItem = yield item1.getLinkedItem(item2.libraryID);
|
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.
2015-06-02 00:09:39 +00:00
|
|
|
assert.equal(linkedItem.id, item2.id);
|
|
|
|
})
|
|
|
|
|
2021-07-21 03:11:20 +00:00
|
|
|
it("shouldn't return a linked item in the trash in another library", async function () {
|
|
|
|
var group = await getGroup();
|
|
|
|
var item1 = await createDataObject('item');
|
|
|
|
var item2 = await createDataObject('item', { libraryID: group.libraryID });
|
|
|
|
var item2URI = Zotero.URI.getItemURI(item2);
|
|
|
|
|
|
|
|
await item2.addLinkedItem(item1);
|
|
|
|
item2.deleted = true;
|
|
|
|
await item2.saveTx();
|
|
|
|
var linkedItem = await item1.getLinkedItem(item2.libraryID);
|
|
|
|
assert.isFalse(linkedItem);
|
|
|
|
})
|
|
|
|
|
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.
2015-06-02 00:09:39 +00:00
|
|
|
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);
|
2017-03-03 21:33:48 +00:00
|
|
|
var linkedItem = yield item2.getLinkedItem(item1.libraryID);
|
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.
2015-06-02 00:09:39 +00:00
|
|
|
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);
|
2017-03-03 21:33:48 +00:00
|
|
|
var linkedItem = yield item2.getLinkedItem(item1.libraryID, true);
|
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.
2015-06-02 00:09:39 +00:00
|
|
|
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);
|
|
|
|
})
|
|
|
|
})
|
2021-01-13 05:40:13 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
describe("#fromJSON()", function () {
|
|
|
|
it("should remove object from trash if 'deleted' property not provided", async function () {
|
|
|
|
for (let type of types) {
|
|
|
|
let obj = await createDataObject(type, { deleted: true });
|
|
|
|
|
|
|
|
assert.isTrue(obj.deleted, type);
|
|
|
|
|
|
|
|
let json = obj.toJSON();
|
|
|
|
delete json.deleted;
|
|
|
|
|
|
|
|
obj.fromJSON(json);
|
|
|
|
await obj.saveTx();
|
|
|
|
|
|
|
|
assert.isFalse(obj.deleted, type);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe("#toJSON()", function () {
|
|
|
|
it("should output 'deleted' as true", function () {
|
|
|
|
for (let type of types) {
|
|
|
|
let obj = createUnsavedDataObject(type);
|
|
|
|
obj.deleted = true;
|
|
|
|
let json = obj.toJSON();
|
|
|
|
assert.isTrue(json.deleted, type);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
it("shouldn't include 'deleted' if not set in default mode", function () {
|
|
|
|
for (let type of types) {
|
|
|
|
let obj = createUnsavedDataObject(type);
|
|
|
|
let json = obj.toJSON();
|
|
|
|
assert.notProperty(json, 'deleted', type);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
describe("'patch' mode", function () {
|
|
|
|
it("should include changed 'deleted' field", async function () {
|
|
|
|
for (let type of types) {
|
|
|
|
let plural = Zotero.DataObjectUtilities.getObjectTypePlural(type)
|
|
|
|
let pluralClass = Zotero[Zotero.Utilities.capitalize(plural)];
|
|
|
|
|
|
|
|
// True to false
|
|
|
|
let obj = createUnsavedDataObject(type)
|
|
|
|
obj.deleted = true;
|
|
|
|
let id = await obj.saveTx();
|
|
|
|
obj = await pluralClass.getAsync(id);
|
|
|
|
let patchBase = obj.toJSON();
|
|
|
|
|
|
|
|
obj.deleted = false;
|
|
|
|
let json = obj.toJSON({
|
|
|
|
patchBase: patchBase
|
|
|
|
})
|
|
|
|
assert.isUndefined(json.title, type);
|
|
|
|
assert.isFalse(json.deleted, type);
|
|
|
|
|
|
|
|
// False to true
|
|
|
|
obj = createUnsavedDataObject(type);
|
|
|
|
obj.deleted = false;
|
|
|
|
id = await obj.saveTx();
|
|
|
|
obj = await pluralClass.getAsync(id);
|
|
|
|
patchBase = obj.toJSON();
|
|
|
|
|
|
|
|
obj.deleted = true;
|
|
|
|
json = obj.toJSON({
|
|
|
|
patchBase: patchBase
|
|
|
|
})
|
|
|
|
assert.isUndefined(json.title, type);
|
|
|
|
assert.isTrue(json.deleted, type);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
2015-05-05 07:17:12 +00:00
|
|
|
})
|