Add PDF attachment properties to Zotero.Item

- .attachmentLastProcessedModificationTime
- .attachmentPageIndex
This commit is contained in:
Dan Stillman 2020-12-01 01:58:55 -05:00
parent 901a10f0b6
commit 9a41dc69fe
5 changed files with 181 additions and 17 deletions

View file

@ -49,6 +49,8 @@ Zotero.Item = function(itemTypeOrID) {
this._attachmentSyncState = 0;
this._attachmentSyncedModificationTime = null;
this._attachmentSyncedHash = null;
this._attachmentLastProcessedModificationTime = null;
this._attachmentPageIndex = null;
// loadCreators
this._creators = [];
@ -336,8 +338,10 @@ Zotero.Item.prototype._parseRowData = function(row) {
case 'libraryID':
case 'itemTypeID':
case 'attachmentSyncState':
case 'attachmentSyncedHash':
case 'attachmentSyncedModificationTime':
case 'attachmentSyncedHash':
case 'attachmentLastProcessedModificationTime':
case 'attachmentPageIndex':
case 'createdByUserID':
case 'lastModifiedByUserID':
break;
@ -1722,8 +1726,9 @@ Zotero.Item.prototype._saveData = Zotero.Promise.coroutine(function* (env) {
if (this._changed.attachmentData) {
let sql = "REPLACE INTO itemAttachments "
+ "(itemID, parentItemID, linkMode, contentType, charsetID, path, "
+ "syncState, storageModTime, storageHash) "
+ "VALUES (?,?,?,?,?,?,?,?,?)";
+ "syncState, storageModTime, storageHash, "
+ "lastProcessedModificationTime, pageIndex) "
+ "VALUES (?,?,?,?,?,?,?,?,?,?,?)";
let linkMode = this.attachmentLinkMode;
let contentType = this.attachmentContentType;
let charsetID = this.attachmentCharset
@ -1733,6 +1738,8 @@ Zotero.Item.prototype._saveData = Zotero.Promise.coroutine(function* (env) {
let syncState = this.attachmentSyncState;
let storageModTime = this.attachmentSyncedModificationTime;
let storageHash = this.attachmentSyncedHash;
let lastProcessedModificationTime = this.attachmentLastProcessedModificationTime;
let pageIndex = this.attachmentPageIndex;
if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE && libraryType != 'user') {
throw new Error("Linked files can only be added to user library");
@ -1747,7 +1754,9 @@ Zotero.Item.prototype._saveData = Zotero.Promise.coroutine(function* (env) {
path ? { string: path } : null,
syncState !== undefined ? syncState : 0,
storageModTime !== undefined ? storageModTime : null,
storageHash || null
storageHash || null,
lastProcessedModificationTime || null,
typeof pageIndex === 'number' ? pageIndex : null
];
yield Zotero.DB.queryAsync(sql, params);
@ -3181,6 +3190,66 @@ Zotero.defineProperty(Zotero.Item.prototype, 'attachmentSyncedHash', {
});
//
// PDF attachment properties
//
for (let name of ['lastProcessedModificationTime', 'pageIndex']) {
let prop = 'attachment' + Zotero.Utilities.capitalize(name);
Zotero.defineProperty(Zotero.Item.prototype, prop, {
get: function () {
if (!this.isFileAttachment()) {
return undefined;
}
return this['_' + prop];
},
set: function (val) {
if (!this.isFileAttachment()) {
throw new Error(`${prop} can only be set for file attachments`);
}
if (this.isEmbeddedImageAttachment()) {
throw new Error(`${prop} cannot be set for embedded-image attachments`);
}
switch (name) {
case 'lastProcessedModificationTime':
if (typeof val != 'number') {
Zotero.debug(val, 2);
throw new Error(`${prop} must be a number`);
}
if (parseInt(val) != val || val < 0) {
Zotero.debug(val, 2);
throw new Error(`${prop} must be a timestamp in milliseconds`);
}
if (val < 10000000000) {
Zotero.logError("attachmentlastProcesedModificationTime should be a timestamp in milliseconds "
+ "-- " + val + " given");
}
break;
case 'pageIndex':
if (typeof val != 'number') {
Zotero.debug(val, 2);
throw new Error(`${prop} must be a number`);
}
break;
}
if (val == this['_' + prop]) {
return;
}
if (!this._changed.attachmentData) {
this._changed.attachmentData = {};
}
this._changed.attachmentData[name] = true;
this['_' + prop] = val;
}
});
}
/**
* Modification time of an attachment file
*
@ -3245,7 +3314,6 @@ Zotero.defineProperty(Zotero.Item.prototype, 'attachmentHash', {
});
/**
* Return plain text of attachment content
*
@ -3563,7 +3631,7 @@ for (let name of ['position']) {
Zotero.defineProperty(Zotero.Item.prototype, 'annotationImageAttachment', {
get: function () {
if (!this.isImageAnnotation()) {
throw new Error("'annotationImageAttachment' is only valid for image annotations");
return undefined;
}
var attachments = this.getAttachments();
if (!attachments.length) {
@ -4625,14 +4693,6 @@ Zotero.Item.prototype.fromJSON = function (json, options = {}) {
this.attachmentLinkMode = linkMode;
break;
case 'contentType':
this.attachmentContentType = val;
break;
case 'charset':
this.attachmentCharset = val;
break;
case 'filename':
if (val === "") {
Zotero.logError("Ignoring empty attachment filename in JSON for item " + this.libraryKey);
@ -4642,8 +4702,14 @@ Zotero.Item.prototype.fromJSON = function (json, options = {}) {
}
break;
case 'contentType':
case 'charset':
case 'path':
this.attachmentPath = val;
this['attachment' + field[0].toUpperCase() + field.substr(1)] = val;
break;
case 'attachmentPageIndex':
this[field] = val;
break;
//
@ -4914,6 +4980,18 @@ Zotero.Item.prototype.toJSON = function (options = {}) {
//obj.md5 = (yield this.attachmentHash) || null;
}
}
// PDF attachment properties
if (this.isFileAttachment()) {
let props = ['pageIndex'];
for (let prop of props) {
let fullProp = 'attachment' + Zotero.Utilities.capitalize(prop);
let val = this[fullProp];
if (val !== null && val !== undefined) {
obj[fullProp] = val;
}
}
}
}
// Notes and embedded attachment notes

View file

@ -76,7 +76,9 @@ Zotero.Items = function() {
attachmentPath: "IA.path AS attachmentPath",
attachmentSyncState: "IA.syncState AS attachmentSyncState",
attachmentSyncedModificationTime: "IA.storageModTime AS attachmentSyncedModificationTime",
attachmentSyncedHash: "IA.storageHash AS attachmentSyncedHash"
attachmentSyncedHash: "IA.storageHash AS attachmentSyncedHash",
attachmentLastProcessedModificationTime: "IA.lastProcessedModificationTime AS attachmentLastProcessedModificationTime",
attachmentPageIndex: "IA.pageIndex AS attachmentPageIndex"
};
}
}, {lazy: true});

View file

@ -3238,6 +3238,10 @@ Zotero.Schema = new function(){
yield Zotero.DB.queryAsync("CREATE TABLE itemAnnotations (\n itemID INTEGER PRIMARY KEY,\n parentItemID INT NOT NULL,\n type INTEGER NOT NULL,\n text TEXT,\n comment TEXT,\n color TEXT,\n pageLabel TEXT,\n sortIndex TEXT NOT NULL,\n position TEXT NOT NULL,\n FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE,\n FOREIGN KEY (parentItemID) REFERENCES itemAttachments(itemID) ON DELETE CASCADE\n)");
yield Zotero.DB.queryAsync("CREATE INDEX itemAnnotations_parentItemID ON itemAnnotations(parentItemID)");
yield Zotero.DB.queryAsync("ALTER TABLE itemAttachments ADD COLUMN lastProcessedModificationTime INT");
yield Zotero.DB.queryAsync("CREATE INDEX itemAttachments_lastProcessedModificationTime ON itemAttachments(lastProcessedModificationTime)");
yield Zotero.DB.queryAsync("ALTER TABLE itemAttachments ADD COLUMN pageIndex INT");
}
// If breaking compatibility or doing anything dangerous, clear minorUpdateFrom

View file

@ -104,6 +104,8 @@ CREATE TABLE itemAttachments (
syncState INT DEFAULT 0,
storageModTime INT,
storageHash TEXT,
lastProcessedModificationTime INT,
pageIndex INT,
FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE,
FOREIGN KEY (parentItemID) REFERENCES items(itemID) ON DELETE CASCADE,
FOREIGN KEY (charsetID) REFERENCES charsets(charsetID) ON DELETE SET NULL
@ -112,6 +114,7 @@ CREATE INDEX itemAttachments_parentItemID ON itemAttachments(parentItemID);
CREATE INDEX itemAttachments_charsetID ON itemAttachments(charsetID);
CREATE INDEX itemAttachments_contentType ON itemAttachments(contentType);
CREATE INDEX itemAttachments_syncState ON itemAttachments(syncState);
CREATE INDEX itemAttachments_lastProcessedModificationTime ON itemAttachments(lastProcessedModificationTime);
CREATE TABLE itemAnnotations (
itemID INTEGER PRIMARY KEY,

View file

@ -1159,6 +1159,25 @@ describe("Zotero.Item", function () {
});
describe("#attachmentLastProcessedModificationTime", function () {
it("should save time in milliseconds", async function () {
var item = await createDataObject('item');
var attachment = await importFileAttachment('test.pdf', { parentID: item.id });
var mtime = Date.now();
attachment.attachmentLastProcessedModificationTime = mtime;
await attachment.saveTx();
assert.equal(attachment.attachmentLastProcessedModificationTime, mtime);
var sql = "SELECT lastProcessedModificationTime FROM itemAttachments WHERE itemID=?";
var dbmtime = await Zotero.DB.valueQueryAsync(sql, attachment.id);
assert.equal(mtime, dbmtime);
});
});
describe("Annotations", function () {
var item;
var attachment;
@ -1709,13 +1728,21 @@ describe("Zotero.Item", function () {
assert.isNull(json.md5);
})
it("shouldn't include filename or path for linked_url attachments", function* () {
it("should include PDF properties", async function () {
var item = await importPDFAttachment();
item.attachmentPageIndex = 4;
var json = item.toJSON();
assert.propertyVal(json, 'attachmentPageIndex', 4);
});
it("shouldn't include filename, path, or PDF properties for linked_url attachments", function* () {
var item = new Zotero.Item('attachment');
item.attachmentLinkMode = 'linked_url';
item.url = "https://www.zotero.org/";
var json = item.toJSON();
assert.notProperty(json, "filename");
assert.notProperty(json, "path");
assert.notProperty(json, 'attachmentPageIndex');
});
it("shouldn't include various properties on embedded-image attachments", async function () {
@ -1731,6 +1758,7 @@ describe("Zotero.Item", function () {
assert.notProperty(json, 'note');
assert.notProperty(json, 'charset');
assert.notProperty(json, 'path');
assert.notProperty(json, 'attachmentPageIndex');
});
});
@ -2343,6 +2371,55 @@ describe("Zotero.Item", function () {
assert.equal(item.getField("bookTitle"), "Publication Title");
});
it("should import attachment content type and path", async function () {
var contentType = 'application/pdf';
var path = OS.Path.join(getTestDataDirectory().path, 'test.pdf');
var json = {
itemType: 'attachment',
linkMode: 'linked_file',
contentType,
path
};
var item = new Zotero.Item();
item.libraryID = Zotero.Libraries.userLibraryID;
item.fromJSON(json, { strict: true });
assert.propertyVal(item, 'attachmentContentType', contentType);
assert.propertyVal(item, 'attachmentPath', path);
});
it("should import other attachment fields", async function () {
var contentType = 'application/pdf';
var json = {
itemType: 'attachment',
linkMode: 'linked_file',
contentType: 'text/plain',
charset: 'utf-8',
path: 'attachments:test.txt'
};
var item = new Zotero.Item();
item.libraryID = Zotero.Libraries.userLibraryID;
item.fromJSON(json, { strict: true });
assert.propertyVal(item, 'attachmentCharset', 'utf-8');
});
it("should import PDF fields", async function () {
var attachment = await importPDFAttachment();
var json = attachment.toJSON();
var item = new Zotero.Item();
item.libraryID = attachment.libraryID;
item.fromJSON(json, { strict: true });
assert.propertyVal(item, 'attachmentPageIndex', null);
json.attachmentPageIndex = 4;
item = new Zotero.Item();
item.libraryID = attachment.libraryID;
item.fromJSON(json, { strict: true });
assert.propertyVal(item, 'attachmentPageIndex', json.attachmentPageIndex);
});
it("should import annotation fields", async function () {
var attachment = await importPDFAttachment();