4630 lines
113 KiB
JavaScript
4630 lines
113 KiB
JavaScript
/*
|
|
***** BEGIN LICENSE BLOCK *****
|
|
|
|
Copyright (c) 2006 Center for History and New Media
|
|
George Mason University, Fairfax, Virginia, USA
|
|
http://chnm.gmu.edu
|
|
|
|
Licensed under the Educational Community License, Version 1.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.opensource.org/licenses/ecl1.php
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
|
|
***** END LICENSE BLOCK *****
|
|
*/
|
|
|
|
/*
|
|
* Constructor for Item object
|
|
*
|
|
* Generally should be called through Zotero.Items rather than directly
|
|
*/
|
|
Zotero.Item = function(itemTypeOrID){
|
|
this._init(itemTypeOrID);
|
|
|
|
if (itemTypeOrID) {
|
|
this._changed.set('itemTypeID');
|
|
}
|
|
}
|
|
|
|
Zotero.Item.prototype._init = function(itemTypeOrID, create) {
|
|
//
|
|
// These members are public so that they can be accessed by public methods
|
|
// -- do not access directly
|
|
//
|
|
this._data = {};
|
|
this.isPrimaryField('itemID'); // make sure primary field hash array exists
|
|
for (var field in Zotero.Item.primaryFields) {
|
|
this._data[field] = null;
|
|
}
|
|
|
|
this._creators = [];
|
|
this._itemData = null;
|
|
|
|
if (itemTypeOrID) {
|
|
// setType initializes type-specific properties in this._itemData
|
|
this.setType(Zotero.ItemTypes.getID(itemTypeOrID), true);
|
|
}
|
|
|
|
this._creatorsLoaded = false;
|
|
this._itemDataLoaded = false;
|
|
|
|
this._changed = new Zotero.Hash();
|
|
this._changedCreators = new Zotero.Hash();
|
|
this._changedItemData = new Zotero.Hash();
|
|
|
|
this._preChangeArray = null;
|
|
|
|
this._noteTitle = null;
|
|
this._noteText = null;
|
|
this._noteAccessTime = null;
|
|
|
|
this._fileLinkMode = null;
|
|
}
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Public Zotero.Item methods
|
|
//
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
|
|
/*
|
|
* Check if the specified field is a primary field from the items table
|
|
*/
|
|
Zotero.Item.prototype.isPrimaryField = function(field){
|
|
// Create primaryFields hash array if not yet created
|
|
if (!Zotero.Item.primaryFields){
|
|
Zotero.Item.primaryFields = Zotero.DB.getColumnHash('items');
|
|
Zotero.Item.primaryFields['firstCreator'] = true;
|
|
Zotero.Item.primaryFields['numNotes'] = true;
|
|
Zotero.Item.primaryFields['numAttachments'] = true;
|
|
}
|
|
|
|
return !!Zotero.Item.primaryFields[field];
|
|
}
|
|
|
|
/*
|
|
* Build object from database
|
|
*/
|
|
Zotero.Item.prototype.loadFromID = function(id) {
|
|
var columns = [], join = [], where = [];
|
|
for (var field in Zotero.Item.primaryFields) {
|
|
var colSQL = null, joinSQL = null, whereSQL = null;
|
|
// If field not already set
|
|
if (this._data[field] === null) {
|
|
// Parts should be the same as query in Zotero.Items._load, just
|
|
// without itemID clause
|
|
switch (field) {
|
|
case 'itemTypeID':
|
|
case 'dateAdded':
|
|
case 'dateModified':
|
|
colSQL = 'I.' + field;
|
|
break;
|
|
|
|
case 'firstCreator':
|
|
colSQL = 'CASE ((SELECT COUNT(*) FROM itemCreators '
|
|
+ 'WHERE itemID=' + id + ')>1) '
|
|
+ "WHEN 0 THEN '' ELSE ' et al.' END AS firstCreator";
|
|
joinSQL = 'LEFT JOIN itemCreators IC ON (I.itemID=IC.itemID) '
|
|
+ 'LEFT JOIN creators C ON (IC.creatorID=C.creatorID)';
|
|
whereSQL = '(IC.orderIndex=0 OR IC.orderIndex IS NULL)';
|
|
break;
|
|
|
|
case 'numNotes':
|
|
colSQL = '(SELECT COUNT(*) FROM itemNotes '
|
|
+ 'WHERE sourceItemID=I.itemID) AS numNotes';
|
|
break;
|
|
|
|
case 'numAttachments':
|
|
colSQL = '(SELECT COUNT(*) FROM itemAttachments '
|
|
+ 'WHERE sourceItemID=I.itemID) AS numAttachments';
|
|
break;
|
|
}
|
|
if (colSQL) {
|
|
columns.push(colSQL);
|
|
}
|
|
if (joinSQL) {
|
|
join.push(joinSQL);
|
|
}
|
|
if (whereSQL) {
|
|
where.push(whereSQL);
|
|
}
|
|
}
|
|
}
|
|
|
|
var sql = 'SELECT I.itemID' + (columns.length ? ', ' + columns.join(', ') : '')
|
|
+ " FROM items I " + (join.length ? join.join(' ') + ' ' : '')
|
|
+ "WHERE I.itemID=" + id + (where.length ? ' AND ' + where.join(' AND ') : '');
|
|
var row = Zotero.DB.rowQuery(sql);
|
|
this.loadFromRow(row);
|
|
}
|
|
|
|
|
|
/*
|
|
* Populate basic item data from a database row
|
|
*/
|
|
Zotero.Item.prototype.loadFromRow = function(row, reload) {
|
|
// If necessary or reloading, set the type, initialize this._itemData,
|
|
// and reset _itemDataLoaded
|
|
if (reload || (!this.getType() && row['itemTypeID'])) {
|
|
this.setType(row['itemTypeID'], true);
|
|
}
|
|
|
|
for (var col in row){
|
|
// Only accept primary field data through loadFromRow()
|
|
if (this.isPrimaryField(col)){
|
|
//Zotero.debug('Setting field ' + col + ' for item ' + this.getID());
|
|
this._data[col] = row[col] ? row[col] : false;
|
|
}
|
|
else {
|
|
Zotero.debug(col + ' is not a valid primary field');
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* Check if any data fields have changed since last save
|
|
*/
|
|
Zotero.Item.prototype.hasChanged = function(){
|
|
return (this._changed.length || this._changedCreators.length ||
|
|
this._changedItemData.length);
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype.getID = function(){
|
|
return this._data['itemID'] ? this._data['itemID'] : false;
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype.getType = function(){
|
|
return this._data['itemTypeID'] ? this._data['itemTypeID'] : false;
|
|
}
|
|
|
|
|
|
/*
|
|
* Set or change the item's type
|
|
*/
|
|
Zotero.Item.prototype.setType = function(itemTypeID, loadIn) {
|
|
if (itemTypeID==this.getType()){
|
|
return true;
|
|
}
|
|
|
|
// If there's an existing type
|
|
if (this.getType()){
|
|
var copiedFields = [];
|
|
|
|
var obsoleteFields = this.getFieldsNotInType(itemTypeID);
|
|
if (obsoleteFields) {
|
|
for each(var oldFieldID in obsoleteFields) {
|
|
// Try to get a base type for this field
|
|
var baseFieldID =
|
|
Zotero.ItemFields.getBaseIDFromTypeAndField(this.getType(), oldFieldID);
|
|
|
|
if (baseFieldID) {
|
|
var newFieldID =
|
|
Zotero.ItemFields.getFieldIDFromTypeAndBase(itemTypeID, baseFieldID);
|
|
|
|
// If so, save value to copy to new field
|
|
if (newFieldID) {
|
|
copiedFields.push([newFieldID, this.getField(oldFieldID)]);
|
|
}
|
|
}
|
|
|
|
// Clear old field
|
|
this.setField(oldFieldID, false);
|
|
}
|
|
}
|
|
|
|
if (!loadIn) {
|
|
for (var fieldID in this._itemData) {
|
|
if (this._itemData[fieldID] &&
|
|
(!obsoleteFields || obsoleteFields.indexOf(fieldID) == -1)) {
|
|
copiedFields.push([fieldID, this.getField(fieldID)]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// And reset custom creator types to the default
|
|
var creators = this.getCreators();
|
|
if (creators){
|
|
for (var i in creators){
|
|
if (!Zotero.CreatorTypes.isValidForItemType(creators[i].creatorTypeID, itemTypeID))
|
|
{
|
|
// Reset to contributor (creatorTypeID 2), which exists in all
|
|
this.setCreator(i, creators[i].firstName,
|
|
creators[i].lastName, 2, creators[i].fieldMode);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
this._data['itemTypeID'] = itemTypeID;
|
|
|
|
// Initialize this._itemData with type-specific fields
|
|
this._itemData = {};
|
|
var fields = Zotero.ItemFields.getItemTypeFields(itemTypeID);
|
|
for each(var fieldID in fields) {
|
|
this._itemData[fieldID] = null;
|
|
}
|
|
|
|
if (copiedFields) {
|
|
for each(var f in copiedFields) {
|
|
this.setField(f[0], f[1]);
|
|
}
|
|
}
|
|
|
|
if (loadIn) {
|
|
this._itemDataLoaded = false;
|
|
}
|
|
else {
|
|
this._changed.set('itemTypeID');
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/*
|
|
* Find existing fields from current type that aren't in another
|
|
*
|
|
* If _allowBaseConversion_, don't return fields that can be converted
|
|
* via base fields (e.g. label => publisher => studio)
|
|
*/
|
|
Zotero.Item.prototype.getFieldsNotInType = function (itemTypeID, allowBaseConversion) {
|
|
var sql = "SELECT fieldID FROM itemTypeFields WHERE itemTypeID=?1 AND "
|
|
+ "fieldID IN (SELECT fieldID FROM itemData WHERE itemID=?2) AND "
|
|
+ "fieldID NOT IN (SELECT fieldID FROM itemTypeFields WHERE itemTypeID=?3)";
|
|
|
|
if (allowBaseConversion) {
|
|
// Not the type-specific field for a base field in the new type
|
|
sql += " AND fieldID NOT IN (SELECT fieldID FROM baseFieldMappings "
|
|
+ "WHERE itemTypeID=?1 AND baseFieldID IN "
|
|
+ "(SELECT fieldID FROM itemTypeFields WHERE itemTypeID=?3)) AND ";
|
|
// And not a base field with a type-specific field in the new type
|
|
sql += "fieldID NOT IN (SELECT baseFieldID FROM baseFieldMappings "
|
|
+ "WHERE itemTypeID=?3) AND ";
|
|
// And not the type-specific field for a base field that has
|
|
// a type-specific field in the new type
|
|
sql += "fieldID NOT IN (SELECT fieldID FROM baseFieldMappings "
|
|
+ "WHERE itemTypeID=?1 AND baseFieldID IN "
|
|
+ "(SELECT baseFieldID FROM baseFieldMappings WHERE itemTypeID=?3))";
|
|
}
|
|
|
|
return Zotero.DB.columnQuery(sql, [this.getType(), this.getID(), {int: itemTypeID}]);
|
|
}
|
|
|
|
|
|
/**
|
|
* Return an array of collectionIDs for all collections the item belongs to
|
|
**/
|
|
Zotero.Item.prototype.getCollections = function(){
|
|
return Zotero.DB.columnQuery("SELECT collectionID FROM collectionItems "
|
|
+ "WHERE itemID=" + this.getID());
|
|
}
|
|
|
|
|
|
/**
|
|
* Determine whether the item belongs to a given collectionID
|
|
**/
|
|
Zotero.Item.prototype.inCollection = function(collectionID){
|
|
return !!parseInt(Zotero.DB.valueQuery("SELECT COUNT(*) "
|
|
+ "FROM collectionItems WHERE collectionID=" + collectionID + " AND "
|
|
+ "itemID=" + this.getID()));
|
|
}
|
|
|
|
|
|
/*
|
|
* Returns the number of creators for this item
|
|
*/
|
|
Zotero.Item.prototype.numCreators = function(){
|
|
if (this.getID() && !this._creatorsLoaded){
|
|
this._loadCreators();
|
|
}
|
|
return this._creators.length;
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype.hasCreatorAt = function(pos){
|
|
if (this.getID() && !this._creatorsLoaded){
|
|
this._loadCreators();
|
|
}
|
|
|
|
return !!this._creators[pos];
|
|
}
|
|
|
|
|
|
/*
|
|
* Returns an array of the creator data at the given position, or false if none
|
|
*
|
|
* Note: Creator data array is returned by reference
|
|
*/
|
|
Zotero.Item.prototype.getCreator = function(pos){
|
|
if (this.getID() && !this._creatorsLoaded){
|
|
this._loadCreators();
|
|
}
|
|
|
|
return this._creators[pos] ? this._creators[pos] : false;
|
|
}
|
|
|
|
|
|
/*
|
|
* Returns a multidimensional array of creators, or an empty array if none
|
|
*
|
|
* Note: Creator data array is returned by reference
|
|
*/
|
|
Zotero.Item.prototype.getCreators = function(){
|
|
if (this.getID() && !this._creatorsLoaded){
|
|
this._loadCreators();
|
|
}
|
|
|
|
var creators = [];
|
|
for (var i=0; i<this._creators.length; i++){
|
|
creators.push(this.getCreator(i));
|
|
}
|
|
return creators;
|
|
}
|
|
|
|
|
|
/*
|
|
* Set or update the creator at the specified position
|
|
*
|
|
* _orderIndex_: the position of this creator in the item (from 0)
|
|
* _creatorTypeID_: id or type name
|
|
* _fieldMode_: 0 for double-field, 1 for single-field mode (default 0)
|
|
*
|
|
* If fieldMode==1, _firstName_ is ignored
|
|
*/
|
|
Zotero.Item.prototype.setCreator = function(orderIndex, firstName, lastName, creatorTypeID, fieldMode){
|
|
if (this.getID() && !this._creatorsLoaded){
|
|
this._loadCreators();
|
|
}
|
|
|
|
// Default to double-field mode if not specified
|
|
if (!fieldMode){
|
|
fieldMode = 0;
|
|
}
|
|
|
|
if (fieldMode==1 || !firstName){
|
|
firstName = '';
|
|
}
|
|
|
|
if (!lastName){
|
|
lastName = '';
|
|
}
|
|
|
|
creatorTypeID = Zotero.CreatorTypes.getID(creatorTypeID);
|
|
|
|
// If creator at this position hasn't changed, cancel
|
|
if (this._creators[orderIndex] &&
|
|
this._creators[orderIndex]['firstName']==firstName &&
|
|
this._creators[orderIndex]['lastName']==lastName &&
|
|
this._creators[orderIndex]['creatorTypeID']==creatorTypeID &&
|
|
this._creators[orderIndex]['fieldMode']==fieldMode){
|
|
return false;
|
|
}
|
|
|
|
if (!creatorTypeID){
|
|
creatorTypeID = 1;
|
|
}
|
|
|
|
var creator = {
|
|
firstName: firstName,
|
|
lastName: lastName,
|
|
creatorTypeID: creatorTypeID,
|
|
fieldMode: fieldMode
|
|
}
|
|
|
|
this._creators[orderIndex] = creator;
|
|
this._changedCreators.set(orderIndex);
|
|
return true;
|
|
}
|
|
|
|
|
|
/*
|
|
* Remove a creator and shift others down
|
|
*/
|
|
Zotero.Item.prototype.removeCreator = function(orderIndex){
|
|
if (this.getID() && !this._creatorsLoaded){
|
|
this._loadCreators();
|
|
}
|
|
|
|
if (!this._creators[orderIndex]){
|
|
throw ('No creator exists at position ' + orderIndex);
|
|
}
|
|
this._creators[orderIndex] = false;
|
|
|
|
// Go to length+1 so we clear the last one
|
|
for (var i=orderIndex, max=this._creators.length+1; i<max; i++){
|
|
var next = this._creators[i+1] ? this._creators[i+1] : false;
|
|
this._creators[i] = next;
|
|
this._changedCreators.set(i);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
// Currently unused
|
|
Zotero.Item.prototype.creatorExists = function(firstName, lastName, creatorTypeID, fieldMode, skipIndex){
|
|
if (fieldMode==1 || !firstName){
|
|
firstName = '';
|
|
}
|
|
|
|
if (!lastName){
|
|
lastName = '';
|
|
}
|
|
|
|
for (var j=0, len=this.numCreators(); j<len; j++){
|
|
if (typeof skipIndex!='undefined' && skipIndex==j){
|
|
continue;
|
|
}
|
|
|
|
var creator2 = this.getCreator(j);
|
|
|
|
if (firstName==creator2['firstName'] &&
|
|
lastName==creator2['lastName'] &&
|
|
creatorTypeID==creator2['creatorTypeID'] &&
|
|
fieldMode==creator2['fieldMode']){
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
/*
|
|
* Retrieves (and loads from DB, if necessary) an itemData field value
|
|
*
|
|
* Field can be passed as fieldID or fieldName
|
|
*
|
|
* If |unformatted| is true, skip any special processing of DB value
|
|
* (e.g. multipart date field) (default false)
|
|
*
|
|
* If |includeBaseMapped| is true and field is a base field, returns value of
|
|
* type-specific field instead (e.g. 'label' for 'publisher' in 'audioRecording')
|
|
*/
|
|
Zotero.Item.prototype.getField = function(field, unformatted, includeBaseMapped) {
|
|
//Zotero.debug('Requesting field ' + field + ' for item ' + this.getID(), 4);
|
|
if (this.isPrimaryField(field)){
|
|
if (this.getID() && this._data[field] === null) {
|
|
this.loadFromID(this.getID());
|
|
}
|
|
//Zotero.debug('Returning ' + (this._data[field] ? this._data[field] : ''));
|
|
return this._data[field] ? this._data[field] : '';
|
|
}
|
|
|
|
if (this.isNote()) {
|
|
switch (Zotero.ItemFields.getName(field)) {
|
|
case 'title':
|
|
return this.getNoteTitle();
|
|
|
|
default:
|
|
return '';
|
|
}
|
|
}
|
|
|
|
if (includeBaseMapped) {
|
|
var fieldID = Zotero.ItemFields.getFieldIDFromTypeAndBase(
|
|
this.getType(), field
|
|
);
|
|
}
|
|
|
|
if (!fieldID) {
|
|
var fieldID = Zotero.ItemFields.getID(field);
|
|
}
|
|
|
|
if (typeof this._itemData[fieldID] == 'undefined') {
|
|
//Zotero.debug('Returning blank for ' + field + ' in ' + this.getType());
|
|
return '';
|
|
}
|
|
|
|
if (this.getID() && this._itemData[fieldID] === null && !this._itemDataLoaded) {
|
|
this._loadItemData();
|
|
}
|
|
|
|
var value = this._itemData[fieldID] ? this._itemData[fieldID] : '';
|
|
|
|
if (!unformatted){
|
|
// Multipart date fields
|
|
if (Zotero.ItemFields.isFieldOfBase(fieldID, 'date')) {
|
|
value = Zotero.Date.multipartToStr(value);
|
|
}
|
|
}
|
|
//Zotero.debug('Returning ' + value);
|
|
return value;
|
|
}
|
|
|
|
|
|
/*
|
|
* Set a field value, loading existing itemData first if necessary
|
|
*
|
|
* Field can be passed as fieldID or fieldName
|
|
*/
|
|
Zotero.Item.prototype.setField = function(field, value, loadIn){
|
|
if (!field){
|
|
throw ("Field not specified in Item.setField()");
|
|
}
|
|
|
|
// Primary field
|
|
if (this.isPrimaryField(field)){
|
|
throw ('Primary field ' + field + ' cannot be changed through setField');
|
|
}
|
|
|
|
if (!this.getType()){
|
|
throw ('Item type must be set before setting field data.');
|
|
}
|
|
|
|
// If existing item, load field data first unless we're already in
|
|
// the middle of a load
|
|
if (this.getID() && !loadIn && !this._itemDataLoaded) {
|
|
this._loadItemData();
|
|
}
|
|
|
|
var fieldID = Zotero.ItemFields.getID(field);
|
|
|
|
if (!fieldID){
|
|
throw (field + ' is not a valid itemData field.');
|
|
}
|
|
|
|
if (loadIn && this.isNote() && field == 110) { // title
|
|
this._noteTitle = value;
|
|
return true;
|
|
}
|
|
|
|
if (!Zotero.ItemFields.isValidForType(fieldID, this.getType())){
|
|
throw ('"' + field + "' is not a valid field for type " + this.getType());
|
|
}
|
|
|
|
if (!loadIn){
|
|
// Save date field as multipart date
|
|
if (Zotero.ItemFields.isFieldOfBase(fieldID, 'date') &&
|
|
!Zotero.Date.isMultipart(value)) {
|
|
value = Zotero.Date.strToMultipart(value);
|
|
}
|
|
// Validate access date
|
|
else if (fieldID == Zotero.ItemFields.getID('accessDate')) {
|
|
if (value && (!Zotero.Date.isSQLDate(value) &&
|
|
!Zotero.Date.isSQLDateTime(value) &&
|
|
value != 'CURRENT_TIMESTAMP')) {
|
|
Zotero.debug("Discarding invalid accessDate '" + value
|
|
+ "' in Item.setField()");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// If existing value, make sure it's actually changing
|
|
if ((!this._itemData[fieldID] && !value) ||
|
|
(this._itemData[fieldID] && this._itemData[fieldID]==value)) {
|
|
return false;
|
|
}
|
|
|
|
// Save a copy of the object before modifying
|
|
if (!this._preChangeArray) {
|
|
this._preChangeArray = this.toArray();
|
|
}
|
|
}
|
|
|
|
this._itemData[fieldID] = value;
|
|
|
|
if (!loadIn) {
|
|
this._changedItemData.set(fieldID);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
/*
|
|
* Save changes back to database
|
|
*
|
|
* Returns true on item update or itemID of new item
|
|
*/
|
|
Zotero.Item.prototype.save = function(){
|
|
if (!this.hasChanged()){
|
|
Zotero.debug('Item ' + this.getID() + ' has not changed', 4);
|
|
return false;
|
|
}
|
|
|
|
// Make sure there are no gaps in the creator indexes
|
|
var creators = this.getCreators();
|
|
for (var i=0; i<creators.length; i++){
|
|
if (!creators[i] || (!creators[i].firstName && !creators[i].lastName)){
|
|
var lastCreator = true;
|
|
continue;
|
|
}
|
|
if (lastCreator){
|
|
throw("Creator indices not contiguous or don't start at 0");
|
|
}
|
|
}
|
|
|
|
//
|
|
// Existing item, update
|
|
//
|
|
if (this.getID()){
|
|
Zotero.debug('Updating database with new item data', 4);
|
|
|
|
var itemID = this.getID();
|
|
|
|
try {
|
|
Zotero.DB.beginTransaction();
|
|
|
|
// Begin history transaction
|
|
Zotero.History.begin('modify-item', this.getID());
|
|
|
|
//
|
|
// Primary fields
|
|
//
|
|
Zotero.History.modify('items', 'itemID', this.getID());
|
|
|
|
var sql = "UPDATE items SET ";
|
|
var sql2;
|
|
var sqlValues = [];
|
|
|
|
if (this._changed.has('itemTypeID')){
|
|
sql += "itemTypeID=?, ";
|
|
sqlValues.push({'int':this.getField('itemTypeID')});
|
|
}
|
|
|
|
// Always update modified time
|
|
sql += "dateModified=CURRENT_TIMESTAMP ";
|
|
sql += "WHERE itemID=?";
|
|
sqlValues.push({'int':this.getID()});
|
|
|
|
Zotero.DB.query(sql, sqlValues);
|
|
|
|
//
|
|
// Creators
|
|
//
|
|
if (this._changedCreators.length){
|
|
for (orderIndex in this._changedCreators.items){
|
|
Zotero.debug('Creator ' + orderIndex + ' has changed', 4);
|
|
|
|
var creator = this.getCreator(orderIndex);
|
|
|
|
// Delete at position
|
|
Zotero.History.remove('itemCreators', 'itemID-orderIndex',
|
|
[this.getID(), orderIndex]);
|
|
|
|
sql2 = 'DELETE FROM itemCreators'
|
|
+ ' WHERE itemID=' + this.getID()
|
|
+ ' AND orderIndex=' + orderIndex;
|
|
Zotero.DB.query(sql2);
|
|
|
|
// If empty, move on
|
|
if (!creator['firstName'] && !creator['lastName']){
|
|
continue;
|
|
}
|
|
|
|
// See if this is an existing creator
|
|
var creatorID = Zotero.Creators.getID(
|
|
creator['firstName'],
|
|
creator['lastName'],
|
|
creator['fieldMode']
|
|
);
|
|
|
|
// If not, add it
|
|
if (!creatorID){
|
|
creatorID = Zotero.Creators.add(
|
|
creator['firstName'],
|
|
creator['lastName'],
|
|
creator['fieldMode']
|
|
);
|
|
Zotero.History.add('creators', 'creatorID', creatorID);
|
|
}
|
|
|
|
sql = "INSERT INTO itemCreators VALUES (?,?,?,?)";
|
|
|
|
sqlValues = [
|
|
{'int':itemID},
|
|
{'int':creatorID},
|
|
{'int':creator['creatorTypeID']},
|
|
{'int':orderIndex}
|
|
];
|
|
|
|
Zotero.DB.query(sql, sqlValues);
|
|
|
|
Zotero.History.add('itemCreators',
|
|
'itemID-creatorID-creatorTypeID',
|
|
[this.getID(), creatorID, creator['creatorTypeID']]);
|
|
}
|
|
}
|
|
|
|
|
|
//
|
|
// ItemData
|
|
//
|
|
if (this._changedItemData.length){
|
|
var del = new Array();
|
|
|
|
sql = "SELECT valueID FROM itemDataValues WHERE value=?";
|
|
var valueStatement = Zotero.DB.getStatement(sql);
|
|
|
|
sql = "INSERT INTO itemDataValues VALUES (?,?)";
|
|
var insertStatement = Zotero.DB.getStatement(sql);
|
|
|
|
sql = "REPLACE INTO itemData VALUES (?,?,?)";
|
|
var replaceStatement = Zotero.DB.getStatement(sql);
|
|
|
|
for (fieldID in this._changedItemData.items){
|
|
var value = this.getField(fieldID, true);
|
|
if (value) {
|
|
// Field exists
|
|
if (this._preChangeArray[Zotero.ItemFields.getName(fieldID)]) {
|
|
Zotero.History.modify('itemData', 'itemID-fieldID',
|
|
[this.getID(), fieldID]);
|
|
}
|
|
// Field is new
|
|
else {
|
|
Zotero.History.add('itemData', 'itemID-fieldID',
|
|
[this.getID(), fieldID]);
|
|
}
|
|
|
|
valueStatement.bindUTF8StringParameter(0, value);
|
|
if (valueStatement.executeStep()) {
|
|
var valueID = valueStatement.getInt32(0);
|
|
}
|
|
else {
|
|
var valueID = null;
|
|
}
|
|
valueStatement.reset();
|
|
|
|
if (!valueID) {
|
|
valueID = Zotero.getRandomID('itemDataValues', 'valueID', 2097152); // stored in 3 bytes
|
|
insertStatement.bindInt32Parameter(0, valueID);
|
|
|
|
if (Zotero.ItemFields.getID('accessDate') == fieldID
|
|
&& this.getField(fieldID) == 'CURRENT_TIMESTAMP') {
|
|
sql = "INSERT INTO itemDataValues VALUES "
|
|
+ "(?,CURRENT_TIMESTAMP)";
|
|
Zotero.DB.query(sql, {int: valueID});
|
|
}
|
|
else {
|
|
if (Zotero.ItemFields.isInteger(fieldID)) {
|
|
insertStatement.
|
|
bindInt32Parameter(1, value);
|
|
}
|
|
else {
|
|
insertStatement.
|
|
bindUTF8StringParameter(1, value);
|
|
}
|
|
try {
|
|
insertStatement.execute();
|
|
}
|
|
catch (e) {
|
|
throw (Zotero.DB.getLastErrorString());
|
|
}
|
|
}
|
|
}
|
|
|
|
replaceStatement.bindInt32Parameter(0, this.getID());
|
|
replaceStatement.bindInt32Parameter(1, fieldID);
|
|
replaceStatement.bindInt32Parameter(2, valueID);
|
|
|
|
try {
|
|
replaceStatement.execute();
|
|
}
|
|
catch (e) {
|
|
throw (Zotero.DB.getLastErrorString());
|
|
}
|
|
}
|
|
|
|
// If field changed and is empty, mark row for deletion
|
|
else {
|
|
del.push(fieldID);
|
|
}
|
|
}
|
|
|
|
insertStatement.reset();
|
|
replaceStatement.reset();
|
|
|
|
// Delete blank fields
|
|
if (del.length){
|
|
// Add to history
|
|
for (var i in del){
|
|
Zotero.History.remove('itemData', 'itemID-fieldID',
|
|
[this.getID(), del[i]]);
|
|
}
|
|
|
|
sql = 'DELETE from itemData '
|
|
+ 'WHERE itemID=' + this.getID() + ' '
|
|
+ 'AND fieldID IN (' + del.join() + ")";
|
|
Zotero.DB.query(sql);
|
|
}
|
|
}
|
|
|
|
Zotero.History.commit();
|
|
Zotero.DB.commitTransaction();
|
|
}
|
|
catch (e){
|
|
Zotero.History.cancel();
|
|
Zotero.DB.rollbackTransaction();
|
|
throw(e);
|
|
}
|
|
}
|
|
|
|
//
|
|
// New item, insert and return id
|
|
//
|
|
else {
|
|
Zotero.debug('Saving data for new item to database');
|
|
|
|
var isNew = true;
|
|
var sqlColumns = new Array();
|
|
var sqlValues = new Array();
|
|
|
|
//
|
|
// Primary fields
|
|
//
|
|
sqlColumns.push('itemID');
|
|
var itemID = Zotero.getRandomID('items', 'itemID');
|
|
sqlValues.push(itemID);
|
|
|
|
sqlColumns.push('itemTypeID');
|
|
sqlValues.push({'int':this.getField('itemTypeID')});
|
|
|
|
try {
|
|
Zotero.DB.beginTransaction();
|
|
|
|
// Begin history transaction
|
|
// No associated id yet, so we use false
|
|
Zotero.History.begin('add-item', false);
|
|
|
|
//
|
|
// Primary fields
|
|
//
|
|
var sql = "INSERT INTO items (" + sqlColumns.join() + ')'
|
|
+ ' VALUES (';
|
|
// Insert placeholders for bind parameters
|
|
for (var i=0; i<sqlValues.length; i++){
|
|
sql += '?,';
|
|
}
|
|
sql = sql.substring(0,sql.length-1) + ")";
|
|
|
|
// Save basic data to items table
|
|
Zotero.DB.query(sql, sqlValues);
|
|
this._data['itemID'] = itemID;
|
|
|
|
Zotero.History.setAssociatedID(itemID);
|
|
Zotero.History.add('items', 'itemID', itemID);
|
|
|
|
//
|
|
// ItemData
|
|
//
|
|
if (this._changedItemData.length){
|
|
// Use manual bound parameters to speed things up
|
|
sql = "SELECT valueID FROM itemDataValues WHERE value=?";
|
|
var valueStatement = Zotero.DB.getStatement(sql);
|
|
|
|
sql = "INSERT INTO itemDataValues VALUES (?,?)";
|
|
var insertValueStatement = Zotero.DB.getStatement(sql);
|
|
|
|
sql = "INSERT INTO itemData VALUES (?,?,?)";
|
|
var insertStatement = Zotero.DB.getStatement(sql);
|
|
|
|
for (fieldID in this._changedItemData.items){
|
|
var value = this.getField(fieldID, true);
|
|
if (!value) {
|
|
continue;
|
|
}
|
|
|
|
valueStatement.bindUTF8StringParameter(0, value);
|
|
if (valueStatement.executeStep()) {
|
|
var valueID = valueStatement.getInt32(0);
|
|
}
|
|
else {
|
|
var valueID = null;
|
|
}
|
|
valueStatement.reset();
|
|
|
|
if (!valueID) {
|
|
valueID = Zotero.getRandomID('itemDataValues', 'valueID', 2097152); // stored in 3 bytes
|
|
insertValueStatement.bindInt32Parameter(0, valueID);
|
|
|
|
if (Zotero.ItemFields.getID('accessDate') == fieldID
|
|
&& this.getField(fieldID) == 'CURRENT_TIMESTAMP') {
|
|
sql = "INSERT INTO itemDataValues VALUES "
|
|
+ "(?,CURRENT_TIMESTAMP)";
|
|
Zotero.DB.query(sql, {int: valueID});
|
|
}
|
|
else {
|
|
if (Zotero.ItemFields.isInteger(fieldID)) {
|
|
insertValueStatement.
|
|
bindInt32Parameter(1, value);
|
|
}
|
|
else {
|
|
insertValueStatement.
|
|
bindUTF8StringParameter(1, value);
|
|
}
|
|
try {
|
|
insertValueStatement.execute();
|
|
}
|
|
catch (e) {
|
|
throw (Zotero.DB.getLastErrorString());
|
|
}
|
|
}
|
|
}
|
|
|
|
insertStatement.bindInt32Parameter(0, this.getID());
|
|
insertStatement.bindInt32Parameter(1, fieldID);
|
|
insertStatement.bindInt32Parameter(2, valueID);
|
|
|
|
try {
|
|
insertStatement.execute();
|
|
}
|
|
catch(e) {
|
|
throw(Zotero.DB.getLastErrorString());
|
|
}
|
|
|
|
Zotero.History.add('itemData', 'itemID-fieldID',
|
|
[itemID, fieldID]);
|
|
}
|
|
|
|
insertValueStatement.reset();
|
|
insertStatement.reset();
|
|
}
|
|
|
|
//
|
|
// Creators
|
|
//
|
|
if (this._changedCreators.length){
|
|
for (orderIndex in this._changedCreators.items){
|
|
var creator = this.getCreator(orderIndex);
|
|
|
|
// If empty, skip
|
|
if (!creator['firstName'] && !creator['lastName']){
|
|
continue;
|
|
}
|
|
|
|
// See if this is an existing creator
|
|
var creatorID = Zotero.Creators.getID(
|
|
creator['firstName'],
|
|
creator['lastName'],
|
|
creator['fieldMode']
|
|
);
|
|
|
|
// If not, add it
|
|
if (!creatorID){
|
|
creatorID = Zotero.Creators.add(
|
|
creator['firstName'],
|
|
creator['lastName'],
|
|
creator['fieldMode']
|
|
);
|
|
Zotero.History.add('creators', 'creatorID', creatorID);
|
|
}
|
|
|
|
sql = 'INSERT INTO itemCreators VALUES ('
|
|
+ itemID + ',' + creatorID + ','
|
|
+ creator['creatorTypeID'] + ',' + orderIndex
|
|
+ ")";
|
|
Zotero.DB.query(sql);
|
|
|
|
Zotero.History.add('itemCreators',
|
|
'itemID-creatorID-creatorTypeID',
|
|
[this.getID(), creatorID, creator['creatorTypeID']]);
|
|
}
|
|
}
|
|
|
|
Zotero.History.commit();
|
|
Zotero.DB.commitTransaction();
|
|
|
|
// Reload collection to update isEmpty,
|
|
// in case this was the first item in a collection
|
|
Zotero.Collections.reloadAll();
|
|
}
|
|
catch (e){
|
|
Zotero.History.cancel();
|
|
Zotero.DB.rollbackTransaction();
|
|
throw(e);
|
|
}
|
|
}
|
|
|
|
Zotero.Items.reload(this.getID());
|
|
|
|
if (isNew){
|
|
Zotero.Notifier.trigger('add', 'item', this.getID());
|
|
return this.getID();
|
|
}
|
|
else {
|
|
Zotero.Notifier.trigger('modify', 'item', this.getID(), { old: this._preChangeArray });
|
|
return true;
|
|
}
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype.updateDateModified = function(){
|
|
Zotero.DB.query("UPDATE items SET dateModified=CURRENT_TIMESTAMP "
|
|
+ "WHERE itemID=" + this.getID());
|
|
var date = Zotero.DB.valueQuery("SELECT dateModified FROM items "
|
|
+ "WHERE itemID=" + this.getID());
|
|
this._data['dateModified'] = date;
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype.isRegularItem = function(){
|
|
return !(this.isNote() || this.isAttachment());
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype.numChildren = function(){
|
|
return this.numNotes() + this.numAttachments();
|
|
}
|
|
|
|
|
|
////////////////////////////////////////////////////////
|
|
//
|
|
// Methods dealing with note items
|
|
//
|
|
// save() is not required for note functions
|
|
//
|
|
////////////////////////////////////////////////////////
|
|
Zotero.Item.prototype.incrementNoteCount = function(){
|
|
this._data['numNotes']++;
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype.decrementNoteCount = function(){
|
|
this._data['numNotes']--;
|
|
}
|
|
|
|
|
|
/**
|
|
* Determine if an item is a note
|
|
**/
|
|
Zotero.Item.prototype.isNote = function(){
|
|
return Zotero.ItemTypes.getName(this.getType())=='note';
|
|
}
|
|
|
|
|
|
/**
|
|
* Update an item note
|
|
*
|
|
* Note: This can only be called on notes and attachments
|
|
**/
|
|
Zotero.Item.prototype.updateNote = function(text){
|
|
if (!this.isNote() && !this.isAttachment()){
|
|
throw ("updateNote() can only be called on notes and attachments");
|
|
}
|
|
|
|
if (!this.getID()){
|
|
throw ("Cannot call updateNote() on unsaved item");
|
|
}
|
|
|
|
Zotero.DB.beginTransaction();
|
|
|
|
var preItemArray = this.toArray();
|
|
|
|
var title = Zotero.Notes.noteToTitle(text);
|
|
|
|
if (this.isNote()){
|
|
var sourceItemID = this.getSource();
|
|
|
|
Zotero.DB.query("REPLACE INTO itemNoteTitles VALUES (?,?)",
|
|
[this.getID(), {string: title}]);
|
|
}
|
|
|
|
if (sourceItemID)
|
|
{
|
|
var sql = "REPLACE INTO itemNotes VALUES (?,?,?)";
|
|
var bindParams = [this.getID(), sourceItemID, {string:text}];
|
|
}
|
|
else
|
|
{
|
|
var sql = "REPLACE INTO itemNotes (note, itemID) VALUES (?,?)";
|
|
var bindParams = [{string:text}, this.getID()];
|
|
}
|
|
|
|
var updated = Zotero.DB.query(sql, bindParams);
|
|
if (updated){
|
|
this.updateDateModified();
|
|
Zotero.DB.commitTransaction();
|
|
|
|
this._noteText = text ? text : '';
|
|
this._noteTitle = title ? title : '';
|
|
|
|
Zotero.Notifier.trigger('modify', 'item', this.getID(), { old: preItemArray });
|
|
}
|
|
else {
|
|
Zotero.DB.commitTransaction();
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* Update the cached value of the note
|
|
*/
|
|
Zotero.Item.prototype.updateNoteCache = function(text, title) {
|
|
this._noteText = text ? text : '';
|
|
this._noteTitle = title ? title : '';
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype.setSource = function(sourceItemID){
|
|
if (this.isNote()){
|
|
var type = 'note';
|
|
var Type = 'Note';
|
|
}
|
|
else if (this.isAttachment()){
|
|
var type = 'attachment';
|
|
var Type = 'Attachment';
|
|
}
|
|
else {
|
|
throw ("setSource() can only be called on items of type 'note' or 'attachment'");
|
|
}
|
|
|
|
if (!this.getID()){
|
|
throw ("Cannot call setSource() on unsaved " + type);
|
|
}
|
|
|
|
Zotero.DB.beginTransaction();
|
|
|
|
var preItemArray = this.toArray();
|
|
|
|
var newItem = Zotero.Items.get(sourceItemID);
|
|
// FK check
|
|
if (newItem) {
|
|
if (sourceItemID) {
|
|
var preNewItemArray = newItem.toArray();
|
|
}
|
|
else {
|
|
Zotero.DB.rollbackTransaction();
|
|
throw ("Cannot set " + type + " source to invalid item " + sourceItemID);
|
|
}
|
|
}
|
|
|
|
var oldSourceItemID = this.getSource();
|
|
|
|
if (oldSourceItemID==sourceItemID){
|
|
Zotero.debug(Type + " source hasn't changed", 4);
|
|
Zotero.DB.commitTransaction();
|
|
return false;
|
|
}
|
|
|
|
var oldItem = Zotero.Items.get(oldSourceItemID);
|
|
if (oldSourceItemID && oldItem) {
|
|
var preOldItemArray = oldItem.toArray();
|
|
}
|
|
else {
|
|
var preOldItemArray = false;
|
|
Zotero.debug("Old source item " + oldSourceItemID + "didn't exist in setSource()", 2);
|
|
}
|
|
|
|
// If this was an independent item, remove from any collections where it
|
|
// existed previously and add source instead if there is one
|
|
if (!oldSourceItemID){
|
|
var sql = "SELECT collectionID FROM collectionItems WHERE itemID=?";
|
|
var changedCollections = Zotero.DB.columnQuery(sql, this.getID());
|
|
if (changedCollections){
|
|
if (sourceItemID){
|
|
var sql = "UPDATE OR REPLACE collectionItems "
|
|
+ "SET itemID=? WHERE itemID=?";
|
|
Zotero.DB.query(sql, [sourceItemID, this.getID()]);
|
|
}
|
|
else {
|
|
var sql = "DELETE FROM collectionItems WHERE itemID=?";
|
|
Zotero.DB.query(sql, this.getID());
|
|
}
|
|
}
|
|
}
|
|
|
|
var sql = "UPDATE item" + Type + "s SET sourceItemID=? WHERE itemID=?";
|
|
var bindParams = [sourceItemID ? {int:sourceItemID} : null, this.getID()];
|
|
Zotero.DB.query(sql, bindParams);
|
|
this.updateDateModified();
|
|
Zotero.DB.commitTransaction();
|
|
|
|
Zotero.Notifier.trigger('modify', 'item', this.getID(), { old: preItemArray });
|
|
|
|
// Update the counts of the previous and new sources
|
|
if (oldItem){
|
|
switch (type){
|
|
case 'note':
|
|
oldItem.decrementNoteCount();
|
|
break;
|
|
case 'attachment':
|
|
oldItem.decrementAttachmentCount();
|
|
break;
|
|
}
|
|
Zotero.Notifier.trigger('modify', 'item', oldSourceItemID, { old: preOldItemArray });
|
|
}
|
|
|
|
if (newItem){
|
|
switch (type){
|
|
case 'note':
|
|
newItem.incrementNoteCount();
|
|
break;
|
|
case 'attachment':
|
|
newItem.incrementAttachmentCount();
|
|
break;
|
|
}
|
|
Zotero.Notifier.trigger('modify', 'item', sourceItemID, { old: preNewItemArray });
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns number of notes in item
|
|
**/
|
|
Zotero.Item.prototype.numNotes = function(){
|
|
if (this.isNote()){
|
|
throw ("numNotes() cannot be called on items of type 'note'");
|
|
}
|
|
|
|
if (!this.getID()){
|
|
return 0;
|
|
}
|
|
|
|
return this._data['numNotes'];
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the first line of the note for display in the items list
|
|
**/
|
|
Zotero.Item.prototype.getNoteTitle = function(){
|
|
if (!this.isNote() && !this.isAttachment()){
|
|
throw ("getNoteTitle() can only be called on notes and attachments");
|
|
}
|
|
|
|
if (this._noteTitle !== null){
|
|
return this._noteTitle;
|
|
}
|
|
|
|
var sql = "SELECT title FROM itemNoteTitles WHERE itemID=" + this.getID();
|
|
var title = Zotero.DB.valueQuery(sql);
|
|
|
|
this._noteTitle = title ? title : '';
|
|
|
|
return title ? title : '';
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the text of an item note
|
|
**/
|
|
Zotero.Item.prototype.getNote = function(){
|
|
if (!this.isNote() && !this.isAttachment()){
|
|
throw ("getNote() can only be called on notes and attachments");
|
|
}
|
|
|
|
if (!this.getID()) {
|
|
return '';
|
|
}
|
|
|
|
// Store access time for later garbage collection
|
|
this._noteAccessTime = new Date();
|
|
|
|
if (this._noteText !== null){
|
|
return this._noteText;
|
|
}
|
|
|
|
var sql = "SELECT note FROM itemNotes WHERE itemID=" + this.getID();
|
|
var note = Zotero.DB.valueQuery(sql);
|
|
|
|
this._noteText = note ? note : '';
|
|
|
|
return note ? note : '';
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the itemID of the source item for a note or file
|
|
**/
|
|
Zotero.Item.prototype.getSource = function(){
|
|
if (!this.getID()) {
|
|
return false;
|
|
}
|
|
|
|
if (this.isNote()){
|
|
var Type = 'Note';
|
|
}
|
|
else if (this.isAttachment()){
|
|
var Type = 'Attachment';
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
|
|
var sql = "SELECT sourceItemID FROM item" + Type + "s WHERE itemID=" + this.getID();
|
|
return Zotero.DB.valueQuery(sql);
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns an array of note itemIDs for this item
|
|
**/
|
|
Zotero.Item.prototype.getNotes = function(){
|
|
if (this.isNote()){
|
|
throw ("getNotes() cannot be called on items of type 'note'");
|
|
}
|
|
|
|
if (!this.getID()){
|
|
return [];
|
|
}
|
|
|
|
var sql = "SELECT itemID FROM itemNotes NATURAL JOIN items "
|
|
+ "WHERE sourceItemID=" + this.getID() + " ORDER BY dateAdded";
|
|
return Zotero.DB.columnQuery(sql);
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////
|
|
//
|
|
// Methods dealing with attachments
|
|
//
|
|
// save() is not required for attachment functions
|
|
//
|
|
///////////////////////////////////////////////////////
|
|
Zotero.Item.prototype.incrementAttachmentCount = function(){
|
|
this._data['numAttachments']++;
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype.decrementAttachmentCount = function(){
|
|
this._data['numAttachments']--;
|
|
}
|
|
|
|
|
|
/**
|
|
* Determine if an item is an attachment
|
|
**/
|
|
Zotero.Item.prototype.isAttachment = function(){
|
|
return Zotero.ItemTypes.getName(this.getType())=='attachment';
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns number of files in item
|
|
**/
|
|
Zotero.Item.prototype.numAttachments = function(){
|
|
if (this.isAttachment()){
|
|
throw ("numAttachments() cannot be called on items of type 'attachment'");
|
|
}
|
|
|
|
if (!this.getID()){
|
|
return 0;
|
|
}
|
|
|
|
return this._data['numAttachments'];
|
|
}
|
|
|
|
|
|
/**
|
|
* Get an nsILocalFile for the attachment, or false if the associated file
|
|
* doesn't exist
|
|
*
|
|
* _row_ is optional itemAttachments row if available to skip query
|
|
*
|
|
* Note: Always returns false for items with LINK_MODE_LINKED_URL,
|
|
* since they have no files -- use getField('url') instead
|
|
**/
|
|
Zotero.Item.prototype.getFile = function(row, skipExistsCheck) {
|
|
if (!this.isAttachment()){
|
|
throw ("getFile() can only be called on items of type 'attachment'");
|
|
}
|
|
|
|
if (!row){
|
|
var sql = "SELECT linkMode, path FROM itemAttachments WHERE itemID="
|
|
+ this.getID();
|
|
var row = Zotero.DB.rowQuery(sql);
|
|
}
|
|
|
|
if (!row){
|
|
throw ('Attachment data not found for item ' + this.getID()
|
|
+ ' in getFile()');
|
|
}
|
|
|
|
// No associated files for linked URLs
|
|
if (row['linkMode']==Zotero.Attachments.LINK_MODE_LINKED_URL){
|
|
return false;
|
|
}
|
|
|
|
var file = Components.classes["@mozilla.org/file/local;1"].
|
|
createInstance(Components.interfaces.nsILocalFile);
|
|
|
|
if (row['linkMode']==Zotero.Attachments.LINK_MODE_IMPORTED_URL ||
|
|
row['linkMode']==Zotero.Attachments.LINK_MODE_IMPORTED_FILE){
|
|
try {
|
|
var storageDir = Zotero.getStorageDirectory();
|
|
storageDir.QueryInterface(Components.interfaces.nsILocalFile);
|
|
file.setRelativeDescriptor(storageDir, row['path']);
|
|
if (!file.exists()){
|
|
throw('Invalid relative descriptor');
|
|
}
|
|
}
|
|
catch (e){
|
|
// See if this is a persistent path
|
|
// (deprecated for imported attachments)
|
|
Zotero.debug('Invalid relative descriptor -- trying persistent');
|
|
try {
|
|
file.persistentDescriptor = row['path'];
|
|
|
|
var storageDir = Zotero.getStorageDirectory();
|
|
storageDir.QueryInterface(Components.interfaces.nsILocalFile);
|
|
var path = file.getRelativeDescriptor(storageDir);
|
|
|
|
// If valid, convert this to a relative descriptor
|
|
if (file.exists()){
|
|
Zotero.DB.query("UPDATE itemAttachments SET path=? WHERE itemID=?",
|
|
[path, this.getID()]);
|
|
}
|
|
}
|
|
catch (e){
|
|
Zotero.debug('Invalid persistent descriptor');
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
try {
|
|
file.persistentDescriptor = row['path'];
|
|
}
|
|
catch (e){
|
|
// See if this is an old relative path (deprecated)
|
|
Zotero.debug('Invalid persistent descriptor -- trying relative');
|
|
try {
|
|
var refDir = (row['linkMode']==this.LINK_MODE_LINKED_FILE)
|
|
? Zotero.getZoteroDirectory() : Zotero.getStorageDirectory();
|
|
file.setRelativeDescriptor(refDir, row['path']);
|
|
// If valid, convert this to a persistent descriptor
|
|
if (file.exists()){
|
|
Zotero.DB.query("UPDATE itemAttachments SET path=? WHERE itemID=?",
|
|
[file.persistentDescriptor, this.getID()]);
|
|
}
|
|
}
|
|
catch (e){
|
|
Zotero.debug('Invalid relative descriptor');
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!skipExistsCheck && !file.exists()){
|
|
return false;
|
|
}
|
|
|
|
return file;
|
|
}
|
|
|
|
|
|
/*
|
|
* Rename file associated with an attachment
|
|
*
|
|
* -1 Destination file exists -- use _force_ to overwrite
|
|
* -2 Error renaming
|
|
* false Attachment file not found or other error
|
|
*/
|
|
Zotero.Item.prototype.renameAttachmentFile = function(newName, overwrite) {
|
|
var file = this.getFile();
|
|
if (!file) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
if (file.leafName == newName) {
|
|
return true;
|
|
}
|
|
|
|
var dest = file.parent;
|
|
dest.append(newName);
|
|
|
|
if (overwrite) {
|
|
dest.remove(null);
|
|
}
|
|
else if (dest.exists()) {
|
|
return -1;
|
|
}
|
|
|
|
file.moveTo(file.parent, newName);
|
|
this.relinkAttachmentFile(file);
|
|
|
|
return true;
|
|
}
|
|
catch (e) {
|
|
return -2;
|
|
}
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype.relinkAttachmentFile = function(file) {
|
|
var linkMode = this.getAttachmentLinkMode();
|
|
|
|
if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) {
|
|
throw('Cannot relink linked URL in Zotero.Items.relinkAttachmentFile()');
|
|
}
|
|
|
|
var path = Zotero.Attachments.getPath(file, linkMode);
|
|
|
|
var sql = "UPDATE itemAttachments SET path=? WHERE itemID=?";
|
|
Zotero.DB.query(sql, [path, this.getID()]);
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
* Return a file:/// URL path to files and snapshots
|
|
*/
|
|
Zotero.Item.prototype.getLocalFileURL = function(){
|
|
if (!this.isAttachment){
|
|
throw ("getLocalFileURL() can only be called on items of type 'attachment'");
|
|
}
|
|
|
|
var file = this.getFile();
|
|
|
|
var nsIFPH = Components.classes["@mozilla.org/network/protocol;1?name=file"]
|
|
.getService(Components.interfaces.nsIFileProtocolHandler);
|
|
|
|
return nsIFPH.getURLSpecFromFile(file);
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the link mode of an attachment
|
|
*
|
|
* Possible return values specified as constants in Zotero.Attachments
|
|
* (e.g. Zotero.Attachments.LINK_MODE_LINKED_FILE)
|
|
**/
|
|
Zotero.Item.prototype.getAttachmentLinkMode = function(){
|
|
if (!this.isAttachment()){
|
|
throw ("getAttachmentLinkMode() can only be called on items of type 'attachment'");
|
|
}
|
|
|
|
if (this._fileLinkMode !==null && this._fileLinkMode !==false){
|
|
return this._fileLinkMode;
|
|
}
|
|
|
|
var sql = "SELECT linkMode FROM itemAttachments WHERE itemID=" + this.getID();
|
|
this._fileLinkMode = Zotero.DB.valueQuery(sql);
|
|
return this._fileLinkMode;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the mime type of an attachment (e.g. text/plain)
|
|
**/
|
|
Zotero.Item.prototype.getAttachmentMimeType = function(){
|
|
if (!this.isAttachment()){
|
|
throw ("getAttachmentMIMEType() can only be called on items of type 'attachment'");
|
|
}
|
|
|
|
var sql = "SELECT mimeType FROM itemAttachments WHERE itemID=" + this.getID();
|
|
return Zotero.DB.valueQuery(sql);
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the character set id of an attachment
|
|
**/
|
|
Zotero.Item.prototype.getAttachmentCharset = function(){
|
|
if (!this.isAttachment()){
|
|
throw ("getAttachmentCharset() can only be called on items of type 'attachment'");
|
|
}
|
|
|
|
var sql = "SELECT charsetID FROM itemAttachments WHERE itemID=" + this.getID();
|
|
return Zotero.DB.valueQuery(sql);
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns an array of attachment itemIDs that have this item as a source
|
|
**/
|
|
Zotero.Item.prototype.getAttachments = function(){
|
|
if (this.isAttachment()){
|
|
throw ("getAttachments() cannot be called on items of type 'attachment'");
|
|
}
|
|
|
|
if (!this.getID()){
|
|
return [];
|
|
}
|
|
|
|
var sql = "SELECT itemID FROM itemAttachments NATURAL JOIN items "
|
|
+ "WHERE sourceItemID=" + this.getID() + " ORDER BY dateAdded";
|
|
return Zotero.DB.columnQuery(sql);
|
|
}
|
|
|
|
|
|
/*
|
|
* Returns the itemID of the latest child snapshot of this item with the
|
|
* same URL as the item itself, or false if none
|
|
*/
|
|
Zotero.Item.prototype.getBestSnapshot = function(){
|
|
if (!this.isRegularItem()){
|
|
throw ("getBestSnapshot() can only be called on regular items");
|
|
}
|
|
|
|
if (!this.getField('url')){
|
|
return false;
|
|
}
|
|
|
|
var sql = "SELECT IA.itemID FROM itemAttachments IA NATURAL JOIN items I "
|
|
+ "LEFT JOIN itemData ID ON (IA.itemID=ID.itemID AND fieldID=1) "
|
|
+ "NATURAL JOIN ItemDataValues "
|
|
+ "WHERE sourceItemID=? AND linkMode=? AND value=? "
|
|
+ "ORDER BY dateAdded DESC LIMIT 1";
|
|
|
|
return Zotero.DB.valueQuery(sql, [this.getID(),
|
|
Zotero.Attachments.LINK_MODE_IMPORTED_URL, {string:this.getField('url')}]);
|
|
}
|
|
|
|
|
|
//
|
|
// Methods dealing with item tags
|
|
//
|
|
// save() is not required for tag functions
|
|
//
|
|
Zotero.Item.prototype.addTag = function(tag, type){
|
|
if (!this.getID()){
|
|
throw ('Cannot add tag to unsaved item in Item.addTag()');
|
|
}
|
|
|
|
if (!tag){
|
|
Zotero.debug('Not saving empty tag in Item.addTag()', 2);
|
|
return false;
|
|
}
|
|
|
|
if (!type) {
|
|
type = 0;
|
|
}
|
|
|
|
if (type !=0 && type !=1) {
|
|
throw ('Invalid tag type in Item.addTag()');
|
|
}
|
|
|
|
Zotero.DB.beginTransaction();
|
|
var tagID = Zotero.Tags.getID(tag, type);
|
|
var existingTypes = Zotero.Tags.getTypes(tag);
|
|
|
|
if (existingTypes) {
|
|
// If existing automatic and adding identical user, remove automatic
|
|
if (type == 0 && existingTypes.indexOf(1) != -1) {
|
|
this.removeTag(Zotero.Tags.getID(tag, 1));
|
|
}
|
|
// If existing user and adding automatic, skip
|
|
else if (type == 1 && existingTypes.indexOf(0) != -1) {
|
|
Zotero.debug('Identical user tag already exists -- skipping automatic tag add');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (!tagID) {
|
|
var tagID = Zotero.Tags.add(tag, type);
|
|
}
|
|
|
|
try {
|
|
var result = this.addTagByID(tagID);
|
|
Zotero.DB.commitTransaction();
|
|
}
|
|
catch (e) {
|
|
Zotero.DB.rollbackTransaction();
|
|
throw (e);
|
|
}
|
|
|
|
return result ? tagID : false;
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype.addTagByID = function(tagID) {
|
|
if (!this.getID()) {
|
|
throw ('Cannot add tag to unsaved item in Item.addTagByID()');
|
|
}
|
|
|
|
if (!tagID) {
|
|
Zotero.debug('Not saving nonexistent tag in Item.addTagByID()', 2);
|
|
return false;
|
|
}
|
|
|
|
var sql = "SELECT COUNT(*) FROM tags WHERE tagID = ?";
|
|
var count = !!Zotero.DB.valueQuery(sql, tagID);
|
|
|
|
if (!count) {
|
|
throw ('Cannot add invalid tag id ' + tagID + ' in Item.addTagByID()');
|
|
}
|
|
|
|
Zotero.DB.beginTransaction();
|
|
|
|
// If INSERT OR IGNORE gave us affected rows, we wouldn't need this...
|
|
if (this.hasTag(tagID)) {
|
|
Zotero.debug('Item ' + this.getID() + ' already has tag ' + tagID + ' in Item.addTagByID()');
|
|
Zotero.DB.commitTransaction();
|
|
return false;
|
|
}
|
|
|
|
var sql = "INSERT INTO itemTags VALUES (?,?)";
|
|
Zotero.DB.query(sql, [this.getID(), tagID]);
|
|
|
|
Zotero.DB.commitTransaction();
|
|
Zotero.Notifier.trigger('modify', 'item', this.getID());
|
|
Zotero.Notifier.trigger('add', 'item-tag', this.getID() + '-' + tagID);
|
|
|
|
return true;
|
|
}
|
|
|
|
Zotero.Item.prototype.hasTag = function(tagID) {
|
|
return this.hasTags(tagID);
|
|
}
|
|
|
|
/*
|
|
* Returns true if the item has one or more of |tagIDs|
|
|
*
|
|
* |tagIDs| can be an int or array of ints
|
|
*/
|
|
Zotero.Item.prototype.hasTags = function(tagIDs) {
|
|
var tagIDs = Zotero.flattenArguments(tagIDs);
|
|
|
|
var sql = "SELECT COUNT(*) FROM itemTags WHERE itemID=? AND tagID IN (";
|
|
var q = [];
|
|
var p = [this.getID()];
|
|
for each(var tagID in tagIDs) {
|
|
q.push('?');
|
|
p.push(tagID);
|
|
}
|
|
sql += q.join();
|
|
sql += ")";
|
|
return !!Zotero.DB.valueQuery(sql, p);
|
|
}
|
|
|
|
Zotero.Item.prototype.getTags = function(){
|
|
if (!this.getID()) {
|
|
return false;
|
|
}
|
|
var sql = "SELECT tagID AS id, tag, tagType AS type FROM tags WHERE tagID IN "
|
|
+ "(SELECT tagID FROM itemTags WHERE itemID=" + this.getID() + ") "
|
|
+ "ORDER BY tag COLLATE NOCASE";
|
|
return Zotero.DB.query(sql);
|
|
}
|
|
|
|
Zotero.Item.prototype.getTagIDs = function(){
|
|
var sql = "SELECT tagID FROM itemTags WHERE itemID=" + this.getID();
|
|
return Zotero.DB.columnQuery(sql);
|
|
}
|
|
|
|
Zotero.Item.prototype.replaceTag = function(oldTagID, newTag){
|
|
if (!this.getID()){
|
|
throw ('Cannot replace tag on unsaved item');
|
|
}
|
|
|
|
if (!newTag){
|
|
Zotero.debug('Not replacing with empty tag', 2);
|
|
return false;
|
|
}
|
|
|
|
Zotero.DB.beginTransaction();
|
|
|
|
var oldTag = Zotero.Tags.getName(oldTagID);
|
|
if (oldTag==newTag){
|
|
Zotero.DB.commitTransaction();
|
|
return false;
|
|
}
|
|
|
|
this.removeTag(oldTagID);
|
|
var id = this.addTag(newTag);
|
|
Zotero.DB.commitTransaction();
|
|
Zotero.Notifier.trigger('modify', 'item', this.getID());
|
|
Zotero.Notifier.trigger('remove', 'item-tag', this.getID() + '-' + oldTagID);
|
|
Zotero.Notifier.trigger('add', 'item-tag', this.getID() + '-' + id);
|
|
return id;
|
|
}
|
|
|
|
Zotero.Item.prototype.removeTag = function(tagID){
|
|
if (!this.getID()){
|
|
throw ('Cannot remove tag on unsaved item');
|
|
}
|
|
|
|
Zotero.DB.beginTransaction();
|
|
var sql = "DELETE FROM itemTags WHERE itemID=? AND tagID=?";
|
|
Zotero.DB.query(sql, [this.getID(), tagID]);
|
|
Zotero.Tags.purge();
|
|
Zotero.DB.commitTransaction();
|
|
Zotero.Notifier.trigger('modify', 'item', this.getID());
|
|
Zotero.Notifier.trigger('remove', 'item-tag', this.getID() + '-' + tagID);
|
|
}
|
|
|
|
Zotero.Item.prototype.removeAllTags = function(){
|
|
if (!this.getID()) {
|
|
throw ('Cannot remove tags on unsaved item');
|
|
}
|
|
|
|
Zotero.DB.beginTransaction();
|
|
var tagIDs = this.getTagIDs();
|
|
if (!tagIDs) {
|
|
Zotero.DB.commitTransaction();
|
|
return;
|
|
}
|
|
|
|
Zotero.DB.query("DELETE FROM itemTags WHERE itemID=?", this.getID());
|
|
Zotero.Tags.purge();
|
|
Zotero.DB.commitTransaction();
|
|
Zotero.Notifier.trigger('modify', 'item', this.getID());
|
|
|
|
for (var i in tagIDs) {
|
|
tagIDs[i] = this.getID() + '-' + tagIDs[i];
|
|
}
|
|
Zotero.Notifier.trigger('remove', 'item-tag', tagIDs);
|
|
}
|
|
|
|
|
|
//
|
|
// Methods dealing with See Also links
|
|
//
|
|
// save() is not required for See Also functions
|
|
//
|
|
Zotero.Item.prototype.addSeeAlso = function(itemID){
|
|
if (itemID==this.getID()){
|
|
Zotero.debug('Cannot add item as See Also of itself', 2);
|
|
return false;
|
|
}
|
|
|
|
Zotero.DB.beginTransaction();
|
|
|
|
var relatedItem = Zotero.Items.get(itemID);
|
|
|
|
if (!relatedItem){
|
|
Zotero.DB.commitTransaction();
|
|
throw ("Cannot add invalid item " + itemID + " as See Also");
|
|
return false;
|
|
}
|
|
|
|
// Check both ways, using a UNION to take advantage of indexes
|
|
var sql = "SELECT (SELECT COUNT(*) FROM itemSeeAlso WHERE itemID=?1 AND "
|
|
+ "linkedItemID=?2) + (SELECT COUNT(*) FROM itemSeeAlso WHERE "
|
|
+ "linkedItemID=?1 AND itemID=?2)";
|
|
if (Zotero.DB.valueQuery(sql, [this.getID(), itemID])){
|
|
Zotero.DB.commitTransaction();
|
|
Zotero.debug("Item " + itemID + " already linked", 2);
|
|
return false;
|
|
}
|
|
|
|
var notifierData = [
|
|
{ old: this.toArray() },
|
|
{ old: relatedItem.toArray() }
|
|
];
|
|
|
|
var sql = "INSERT INTO itemSeeAlso VALUES (?,?)";
|
|
Zotero.DB.query(sql, [this.getID(), {int:itemID}]);
|
|
Zotero.DB.commitTransaction();
|
|
Zotero.Notifier.trigger('modify', 'item', [this.getID(), itemID], notifierData);
|
|
return true;
|
|
}
|
|
|
|
Zotero.Item.prototype.removeSeeAlso = function(itemID){
|
|
if (!this.getID()) {
|
|
throw ('Cannot remove related item of unsaved item');
|
|
}
|
|
|
|
Zotero.DB.beginTransaction();
|
|
|
|
var relatedItem = Zotero.Items.get(itemID);
|
|
if (!relatedItem) {
|
|
Zotero.DB.commitTransaction();
|
|
throw ("Cannot remove invalid item " + itemID + " as See Also");
|
|
return false;
|
|
}
|
|
|
|
var notifierData = [
|
|
{ old: this.toArray() },
|
|
{ old: relatedItem.toArray() }
|
|
];
|
|
|
|
var sql = "DELETE FROM itemSeeAlso WHERE itemID=? AND linkedItemID=?";
|
|
Zotero.DB.query(sql, [this.getID(), itemID]);
|
|
var sql = "DELETE FROM itemSeeAlso WHERE itemID=? AND linkedItemID=?";
|
|
Zotero.DB.query(sql, [itemID, this.getID()]);
|
|
Zotero.DB.commitTransaction();
|
|
Zotero.Notifier.trigger('modify', 'item', [this.getID(), itemID], notifierData);
|
|
}
|
|
|
|
Zotero.Item.prototype.removeAllRelated = function() {
|
|
if (!this.getID()) {
|
|
throw ('Cannot remove related items of unsaved item');
|
|
}
|
|
|
|
Zotero.DB.beginTransaction();
|
|
var relateds = this.getSeeAlso();
|
|
if (!relateds) {
|
|
Zotero.DB.commitTransaction();
|
|
return;
|
|
}
|
|
|
|
var notifierData = [ { old: this.toArray() } ];
|
|
for each(var id in relateds) {
|
|
var item = Zotero.Items.get(id);
|
|
notifierData.push(item ? { old: item.toArray() } : false);
|
|
}
|
|
|
|
Zotero.DB.query("DELETE FROM itemSeeAlso WHERE itemID=?", this.getID());
|
|
Zotero.DB.query("DELETE FROM itemSeeAlso WHERE linkedItemID=?", this.getID());
|
|
Zotero.DB.commitTransaction();
|
|
|
|
var ids = [this.getID()].concat(relateds);
|
|
|
|
Zotero.Notifier.trigger('modify', 'item', ids, notifierData);
|
|
}
|
|
|
|
Zotero.Item.prototype.getSeeAlso = function(){
|
|
if (!this.getID()) {
|
|
return false;
|
|
}
|
|
// Check both ways, using a UNION to take advantage of indexes
|
|
var sql ="SELECT linkedItemID FROM itemSeeAlso WHERE itemID=?1 UNION "
|
|
+ "SELECT itemID FROM itemSeeAlso WHERE linkedItemID=?1";
|
|
return Zotero.DB.columnQuery(sql, this.getID());
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype.getImageSrc = function() {
|
|
var itemType = Zotero.ItemTypes.getName(this.getType());
|
|
if (itemType == 'attachment') {
|
|
var linkMode = this.getAttachmentLinkMode();
|
|
if (linkMode == Zotero.Attachments.LINK_MODE_IMPORTED_FILE) {
|
|
itemType = itemType + "-file";
|
|
}
|
|
else if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE) {
|
|
itemType = itemType + "-link";
|
|
}
|
|
else if (linkMode == Zotero.Attachments.LINK_MODE_IMPORTED_URL) {
|
|
itemType = itemType + "-snapshot";
|
|
}
|
|
else if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) {
|
|
itemType = itemType + "-web-link";
|
|
}
|
|
}
|
|
|
|
return Zotero.ItemTypes.getImageSrc(itemType);
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype.clone = function() {
|
|
if (!this.getID()) {
|
|
throw ('Cannot clone unsaved item in Zotero.Item.clone()');
|
|
}
|
|
|
|
if (this.isAttachment()) {
|
|
throw ('Cloning attachment items not supported in Zotero.Item.clone()');
|
|
}
|
|
|
|
Zotero.DB.beginTransaction();
|
|
|
|
var obj = this.toArray();
|
|
|
|
// Note
|
|
if (this.isNote()) {
|
|
var newItemID = Zotero.Notes.add(this.getNote(), this.getSource());
|
|
var newItem = Zotero.Items.get(newItemID);
|
|
}
|
|
|
|
// Regular item
|
|
else {
|
|
var itemTypeID = this.getType();
|
|
var newItem = new Zotero.Item(itemTypeID);
|
|
|
|
for (var i in obj) {
|
|
switch (i) {
|
|
case 'creators':
|
|
var i = 0;
|
|
for each(var c in obj.creators) {
|
|
newItem.setCreator(i, c.firstName, c.lastName,
|
|
c.creatorType, c.fieldMode ? c.fieldMode : null);
|
|
i++;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
var fieldID = Zotero.ItemFields.getID(i);
|
|
if (fieldID && Zotero.ItemFields.isValidForType(fieldID, itemTypeID)) {
|
|
newItem.setField(i, obj[i]);
|
|
}
|
|
}
|
|
|
|
newItem.save();
|
|
}
|
|
|
|
if (obj.tags) {
|
|
for each(var tag in obj.tags) {
|
|
newItem.addTagByID(tag.id);
|
|
}
|
|
}
|
|
|
|
if (obj.seeAlso) {
|
|
for each(var id in obj.seeAlso) {
|
|
newItem.addSeeAlso(id)
|
|
}
|
|
}
|
|
|
|
Zotero.DB.commitTransaction();
|
|
return newItem.getID();
|
|
}
|
|
|
|
|
|
/**
|
|
* Delete item from database and clear from Zotero.Items internal array
|
|
*
|
|
* Items.erase() should be used instead of this
|
|
**/
|
|
Zotero.Item.prototype.erase = function(deleteChildren){
|
|
if (!this.getID()){
|
|
return false;
|
|
}
|
|
|
|
Zotero.debug('Deleting item ' + this.getID());
|
|
|
|
var changedItems = [];
|
|
|
|
Zotero.DB.beginTransaction();
|
|
|
|
// For Notifier
|
|
var preItemArray = this.toArray();
|
|
var notifierData = [];
|
|
|
|
// Remove item from parent collections
|
|
var parentCollectionIDs = this.getCollections();
|
|
if (parentCollectionIDs){
|
|
for (var i=0; i<parentCollectionIDs.length; i++){
|
|
Zotero.Collections.get(parentCollectionIDs[i]).removeItem(this.getID());
|
|
}
|
|
}
|
|
|
|
// Note
|
|
if (this.isNote()){
|
|
// Decrement note count of source items
|
|
var sql = "SELECT sourceItemID FROM itemNotes WHERE itemID=" + this.getID();
|
|
var sourceItemID = Zotero.DB.valueQuery(sql);
|
|
if (sourceItemID){
|
|
var sourceItem = Zotero.Items.get(sourceItemID);
|
|
notifierData.push({ old: sourceItem.toArray() });
|
|
sourceItem.decrementNoteCount();
|
|
changedItems.push(sourceItemID);
|
|
}
|
|
}
|
|
// Attachment
|
|
else if (this.isAttachment()){
|
|
// Decrement file count of source items
|
|
var sql = "SELECT sourceItemID FROM itemAttachments WHERE itemID=" + this.getID();
|
|
var sourceItemID = Zotero.DB.valueQuery(sql);
|
|
if (sourceItemID){
|
|
var sourceItem = Zotero.Items.get(sourceItemID);
|
|
notifierData.push({ old: sourceItem.toArray() });
|
|
sourceItem.decrementAttachmentCount();
|
|
changedItems.push(sourceItemID);
|
|
}
|
|
|
|
// Delete associated files
|
|
var linkMode = this.getAttachmentLinkMode();
|
|
switch (linkMode){
|
|
// Link only -- nothing to delete
|
|
case Zotero.Attachments.LINK_MODE_LINKED_URL:
|
|
break;
|
|
default:
|
|
try {
|
|
var file = Zotero.getStorageDirectory();
|
|
file.append(this.getID());
|
|
if (file.exists()){
|
|
file.remove(true);
|
|
}
|
|
}
|
|
catch (e) {
|
|
Components.utils.reportError(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Regular item
|
|
|
|
// If flag given, delete child notes and files
|
|
else if (deleteChildren){
|
|
var sql = "SELECT itemID FROM itemNotes WHERE sourceItemID=?1 UNION "
|
|
+ "SELECT itemID FROM itemAttachments WHERE sourceItemID=?1";
|
|
var toDelete = Zotero.DB.columnQuery(sql, [this.getID()]);
|
|
|
|
if (toDelete){
|
|
for (var i in toDelete){
|
|
var obj = Zotero.Items.get(toDelete[i]);
|
|
obj.erase(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Otherwise just unlink any child notes or files without deleting
|
|
else {
|
|
// Notes
|
|
var sql = "SELECT itemID FROM itemNotes WHERE sourceItemID=" + this.getID();
|
|
var childNotes = Zotero.DB.columnQuery(sql);
|
|
if (childNotes){
|
|
for each(var id in childNotes) {
|
|
var i = Zotero.Items.get(id);
|
|
notifierData.push({ old: i.toArray() });
|
|
}
|
|
changedItems.push(childNotes);
|
|
}
|
|
var sql = "UPDATE itemNotes SET sourceItemID=NULL WHERE sourceItemID="
|
|
+ this.getID();
|
|
Zotero.DB.query(sql);
|
|
|
|
// Attachments
|
|
var sql = "SELECT itemID FROM itemAttachments WHERE sourceItemID=" + this.getID();
|
|
var childAttachments = Zotero.DB.columnQuery(sql);
|
|
if (childAttachments){
|
|
for each(var id in childAttachments) {
|
|
var i = Zotero.Items.get(id);
|
|
notifierData.push({ old: i.toArray() });
|
|
}
|
|
changedItems.push(childAttachments);
|
|
}
|
|
var sql = "UPDATE itemAttachments SET sourceItemID=NULL WHERE sourceItemID="
|
|
+ this.getID();
|
|
Zotero.DB.query(sql);
|
|
}
|
|
|
|
// Flag See Also links for notification
|
|
var relateds = this.getSeeAlso();
|
|
if (relateds){
|
|
for each(var id in relateds) {
|
|
var i = Zotero.Items.get(id);
|
|
notifierData.push({ old: i.toArray() });
|
|
}
|
|
changedItems = changedItems.concat(relateds);
|
|
}
|
|
|
|
// Clear fulltext cache
|
|
if (this.isAttachment()) {
|
|
Zotero.Fulltext.clearItemWords(this.getID());
|
|
//Zotero.Fulltext.clearItemContent(this.getID());
|
|
}
|
|
|
|
sql = 'DELETE FROM itemCreators WHERE itemID=' + this.getID() + ";\n";
|
|
sql += 'DELETE FROM itemNotes WHERE itemID=' + this.getID() + ";\n";
|
|
sql += 'DELETE FROM itemNoteTitles WHERE itemID=' + this.getID() + ";\n";
|
|
sql += 'DELETE FROM itemAttachments WHERE itemID=' + this.getID() + ";\n";
|
|
sql += 'DELETE FROM itemSeeAlso WHERE itemID=' + this.getID() + ";\n";
|
|
sql += 'DELETE FROM itemSeeAlso WHERE linkedItemID=' + this.getID() + ";\n";
|
|
sql += 'DELETE FROM itemTags WHERE itemID=' + this.getID() + ";\n";
|
|
sql += 'DELETE FROM itemData WHERE itemID=' + this.getID() + ";\n";
|
|
sql += 'DELETE FROM items WHERE itemID=' + this.getID() + ";\n";
|
|
|
|
Zotero.DB.query(sql);
|
|
|
|
try {
|
|
Zotero.DB.commitTransaction();
|
|
}
|
|
catch (e){
|
|
// On failure, reset count of source items
|
|
if (sourceItem){
|
|
if (this.isNote()){
|
|
sourceItem.incrementNoteCount();
|
|
}
|
|
else if (this.isAttachment()){
|
|
sourceItem.incrementAttachmentCount();
|
|
}
|
|
}
|
|
Zotero.DB.rollbackTransaction();
|
|
throw (e);
|
|
}
|
|
|
|
Zotero.Items.unload(this.getID());
|
|
|
|
// Send notification of changed items
|
|
if (changedItems.length){
|
|
Zotero.Notifier.trigger('modify', 'item', changedItems, notifierData);
|
|
}
|
|
|
|
Zotero.Notifier.trigger('delete', 'item', this.getID(), { old: preItemArray });
|
|
}
|
|
|
|
|
|
Zotero.Item.prototype.isCollection = function(){
|
|
return false;
|
|
}
|
|
|
|
|
|
/**
|
|
* Convert the item data into a multidimensional associative array
|
|
* for use by the export functions
|
|
**/
|
|
Zotero.Item.prototype.toArray = function(){
|
|
if (this.getID() && !this._itemDataLoaded){
|
|
this._loadItemData();
|
|
}
|
|
|
|
var arr = [];
|
|
|
|
// Primary fields
|
|
for (var i in this._data){
|
|
switch (i){
|
|
case 'itemTypeID':
|
|
arr['itemType'] = Zotero.ItemTypes.getName(this._data[i]);
|
|
break;
|
|
|
|
// Skip certain fields
|
|
//case 'firstCreator':
|
|
case 'numNotes':
|
|
case 'numAttachments':
|
|
continue;
|
|
|
|
case 'firstCreator':
|
|
if (!this.isRegularItem()) {
|
|
continue;
|
|
}
|
|
// fall through
|
|
|
|
// For the rest, just copy over
|
|
default:
|
|
arr[i] = this._data[i];
|
|
}
|
|
}
|
|
|
|
// Item metadata
|
|
for (var i in this._itemData){
|
|
arr[Zotero.ItemFields.getName(i)] = this._itemData[i];
|
|
}
|
|
|
|
if (!this.isNote() && !this.isAttachment()){
|
|
// Creators
|
|
arr['creators'] = [];
|
|
var creators = this.getCreators();
|
|
for (var i in creators){
|
|
arr['creators'][i] = [];
|
|
arr['creators'][i]['firstName'] = creators[i]['firstName'];
|
|
arr['creators'][i]['lastName'] = creators[i]['lastName'];
|
|
arr['creators'][i]['fieldMode'] = creators[i]['fieldMode'];
|
|
// Convert creatorTypeIDs to text
|
|
arr['creators'][i]['creatorType'] =
|
|
Zotero.CreatorTypes.getName(creators[i]['creatorTypeID']);
|
|
}
|
|
}
|
|
|
|
// Notes
|
|
if (this.isNote()) {
|
|
arr['note'] = this.getNote();
|
|
if (this.getSource()){
|
|
arr['sourceItemID'] = this.getSource();
|
|
}
|
|
}
|
|
|
|
// Attachments
|
|
if (this.isAttachment()){
|
|
// Attachments can have embedded notes
|
|
arr['note'] = this.getNote();
|
|
|
|
if (this.getSource()){
|
|
arr['sourceItemID'] = this.getSource();
|
|
}
|
|
}
|
|
|
|
// Attach children of regular items
|
|
if (this.isRegularItem()){
|
|
// Append attached notes
|
|
arr['notes'] = [];
|
|
var notes = this.getNotes();
|
|
for (var i in notes){
|
|
var note = Zotero.Items.get(notes[i]);
|
|
arr['notes'].push(note.toArray());
|
|
}
|
|
|
|
arr['attachments'] = [];
|
|
var attachments = this.getAttachments();
|
|
for (var i in attachments){
|
|
var attachment = Zotero.Items.get(attachments[i]);
|
|
arr['attachments'].push(attachment.toArray());
|
|
}
|
|
}
|
|
|
|
arr['tags'] = this.getTags();
|
|
arr['seeAlso'] = this.getSeeAlso();
|
|
|
|
return arr;
|
|
}
|
|
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// Private Zotero.Item methods
|
|
//
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
|
|
/*
|
|
* Load in the creators from the database
|
|
*/
|
|
Zotero.Item.prototype._loadCreators = function(){
|
|
if (!this.getID()){
|
|
throw ('ItemID not set for item before attempting to load creators');
|
|
}
|
|
|
|
var sql = 'SELECT C.creatorID, C.*, creatorTypeID, orderIndex '
|
|
+ 'FROM itemCreators IC '
|
|
+ 'LEFT JOIN creators C USING (creatorID) '
|
|
+ 'WHERE itemID=' + this.getID() + ' ORDER BY orderIndex';
|
|
var creators = Zotero.DB.query(sql);
|
|
|
|
this._creatorsLoaded = true;
|
|
|
|
if (!creators){
|
|
return true;
|
|
}
|
|
|
|
this._creators = [];
|
|
for (var i=0; i<creators.length; i++){
|
|
this._creators[creators[i]['orderIndex']] = {
|
|
firstName: creators[i]['firstName'],
|
|
lastName: creators[i]['lastName'],
|
|
creatorTypeID: creators[i]['creatorTypeID'],
|
|
fieldMode: creators[i]['fieldMode']
|
|
};
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/*
|
|
* Load in the field data from the database
|
|
*/
|
|
Zotero.Item.prototype._loadItemData = function(){
|
|
if (!this.getID()){
|
|
throw ('ItemID not set for object before attempting to load data');
|
|
}
|
|
|
|
var sql = "SELECT fieldID, value FROM itemData NATURAL JOIN itemDataValues "
|
|
+ "WHERE itemID=?";
|
|
var fields = Zotero.DB.query(sql, this.getID());
|
|
|
|
var itemTypeFields = Zotero.ItemFields.getItemTypeFields(this.getType());
|
|
|
|
for each(var field in fields) {
|
|
this.setField(field['fieldID'], field['value'], true);
|
|
}
|
|
|
|
// Mark nonexistent fields as loaded
|
|
for each(var fieldID in itemTypeFields) {
|
|
if (this._itemData[fieldID] === null) {
|
|
this._itemData[fieldID] = false;
|
|
}
|
|
}
|
|
|
|
this._itemDataLoaded = true;
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
* Primary interface for accessing Zotero items
|
|
*/
|
|
Zotero.Items = new function(){
|
|
// Privileged methods
|
|
this.get = get;
|
|
this.getAll = getAll;
|
|
this.add = add;
|
|
this.reload = reload;
|
|
this.cacheFields = cacheFields;
|
|
this.erase = erase;
|
|
this.purge = purge;
|
|
this.unload = unload;
|
|
|
|
// Private members
|
|
var _items = [];
|
|
var _itemsLoaded = false;
|
|
var _cachedFields = [];
|
|
|
|
|
|
/*
|
|
* Retrieves (and loads, if necessary) an arbitrary number of items
|
|
*
|
|
* Can be passed ids as individual parameters or as an array of ids, or both
|
|
*
|
|
* If only one argument and it's an id, return object directly;
|
|
* otherwise, return array
|
|
*/
|
|
function get(){
|
|
var toLoad = new Array();
|
|
var loaded = new Array();
|
|
|
|
if (!arguments[0]){
|
|
Zotero.debug('No arguments provided to Items.get()');
|
|
return false;
|
|
}
|
|
|
|
var ids = Zotero.flattenArguments(arguments);
|
|
|
|
for (var i=0; i<ids.length; i++){
|
|
// Check if already loaded
|
|
if (!_items[ids[i]]){
|
|
toLoad.push(ids[i]);
|
|
}
|
|
}
|
|
|
|
// New items to load
|
|
if (toLoad.length){
|
|
_load(toLoad);
|
|
}
|
|
|
|
// If single id, return the object directly
|
|
if (arguments[0] && typeof arguments[0]!='object'
|
|
&& typeof arguments[1]=='undefined'){
|
|
if (!_items[arguments[0]]) {
|
|
Zotero.debug("Item " + arguments[0] + " doesn't exist", 2);
|
|
return false;
|
|
}
|
|
return _items[arguments[0]];
|
|
}
|
|
|
|
// Otherwise, build return array
|
|
for (i=0; i<ids.length; i++){
|
|
if (!_items[ids[i]]){
|
|
Zotero.debug("Item " + ids[i] + " doesn't exist", 2);
|
|
continue;
|
|
}
|
|
loaded.push(_items[ids[i]]);
|
|
}
|
|
|
|
return loaded;
|
|
}
|
|
|
|
|
|
/*
|
|
* Returns all items in the database
|
|
*/
|
|
function getAll(){
|
|
var sql = 'SELECT itemID FROM items';
|
|
// DEBUG: default order?
|
|
|
|
var ids = Zotero.DB.columnQuery(sql);
|
|
return this.get(ids);
|
|
}
|
|
|
|
|
|
/*
|
|
* Create a new item with optional metadata and pass back the primary reference
|
|
*
|
|
* Using "var item = new Zotero.Item()" and "item.save()" directly results
|
|
* in an orphaned reference to the created item. If other code retrieves the
|
|
* new item with Zotero.Items.get() and modifies it, the original reference
|
|
* will not reflect the changes.
|
|
*
|
|
* Using this method avoids the need to call Zotero.Items.get() after save()
|
|
* in order to get the primary item reference. Since it accepts metadata
|
|
* as a JavaScript object, it also offers a simpler syntax than
|
|
* item.setField() and item.setCreator().
|
|
*
|
|
* Callers with no need for an up-to-date reference after save() (or who
|
|
* don't mind doing an extra Zotero.Items.get()) can use Zotero.Item
|
|
* directly if they prefer.
|
|
*
|
|
* Sample usage:
|
|
*
|
|
* var data = {
|
|
* title: "Shakespeare: The Invention of the Human",
|
|
* publisher: "Riverhead Hardcover",
|
|
* date: '1998-10-26',
|
|
* isbn: 1573221201,
|
|
* pages: 745,
|
|
* creators: [
|
|
* ['Harold', 'Bloom', 'author']
|
|
* ]
|
|
* };
|
|
* var item = Zotero.Items.add('book', data);
|
|
*/
|
|
function add(itemTypeOrID, data) {
|
|
var item = new Zotero.Item(itemTypeOrID);
|
|
for (var field in data){
|
|
if (field == 'creators') {
|
|
var i = 0;
|
|
for each(var creator in data.creators) {
|
|
// TODO: accept format from toArray()
|
|
item.setCreator(i, creator[0], creator[1], creator[2], creator[3] ? creator[3] : null);
|
|
i++;
|
|
}
|
|
}
|
|
else {
|
|
item.setField(field, data[field]);
|
|
}
|
|
}
|
|
var id = item.save();
|
|
|
|
return this.get(id);
|
|
}
|
|
|
|
|
|
/*
|
|
* Reloads data for specified items into internal array
|
|
*
|
|
* Can be passed ids as individual parameters or as an array of ids, or both
|
|
*/
|
|
function reload(){
|
|
if (!arguments[0]){
|
|
return false;
|
|
}
|
|
|
|
var ids = Zotero.flattenArguments(arguments);
|
|
Zotero.debug('Reloading ' + ids);
|
|
_load(ids);
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
function cacheFields(fields, items) {
|
|
_load(items);
|
|
|
|
var primaryFields = [];
|
|
var fieldIDs = [];
|
|
for each(var field in fields) {
|
|
// Check if field already cached
|
|
if (_cachedFields.indexOf(field) != -1) {
|
|
continue;
|
|
}
|
|
|
|
_cachedFields.push(field);
|
|
|
|
if (Zotero.Item.prototype.isPrimaryField(field)) {
|
|
primaryFields.push(field);
|
|
}
|
|
else {
|
|
fieldIDs.push(Zotero.ItemFields.getID(field));
|
|
if (Zotero.ItemFields.isBaseField(field)) {
|
|
fieldIDs = fieldIDs.concat(Zotero.ItemFields.getTypeFieldsFromBase(field));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (primaryFields.length) {
|
|
var sql = "SELECT itemID, " + primaryFields.join(', ') + " FROM items";
|
|
if (items) {
|
|
sql += " WHERE itemID IN (" + items.join() + ")";
|
|
}
|
|
var rows = Zotero.DB.query(sql);
|
|
for each(var row in rows) {
|
|
//Zotero.debug('Calling loadFromRow for item ' + row['itemID']);
|
|
_items[row['itemID']].loadFromRow(row);
|
|
}
|
|
}
|
|
|
|
// All fields already cached
|
|
if (!fieldIDs.length) {
|
|
return;
|
|
}
|
|
|
|
var allItemIDs = Zotero.DB.columnQuery("SELECT itemID FROM items");
|
|
var itemFieldsCached = {};
|
|
|
|
var sql = "SELECT itemID, fieldID, value FROM itemData "
|
|
+ "NATURAL JOIN itemDataValues WHERE ";
|
|
if (items) {
|
|
sql += "itemID IN (" + items.join() + ") AND ";
|
|
}
|
|
sql += "fieldID IN (" + fieldIDs.join() + ")";
|
|
|
|
var itemDataRows = Zotero.DB.query(sql);
|
|
for each(var row in itemDataRows) {
|
|
//Zotero.debug('Setting field for item ' + row['itemID']);
|
|
_items[row['itemID']].setField(row['fieldID'], row['value'], true);
|
|
|
|
if (!itemFieldsCached[row['itemID']]) {
|
|
itemFieldsCached[row['itemID']] = {};
|
|
}
|
|
itemFieldsCached[row['itemID']][row['fieldID']] = true;
|
|
}
|
|
|
|
// If 'title' is one of the fields, load in noteTitles
|
|
if (fields.indexOf('title') != -1) {
|
|
var titleFieldID = Zotero.ItemFields.getID('title');
|
|
var sql = 'SELECT itemID, title FROM itemNoteTitles';
|
|
if (items) {
|
|
sql += " WHERE itemID IN (" + items.join() + ")";
|
|
}
|
|
|
|
var rows = Zotero.DB.query(sql);
|
|
for each(var row in rows) {
|
|
//Zotero.debug('Setting title for note ' + row['itemID']);
|
|
_items[row['itemID']].setField(titleFieldID, row['title'], true);
|
|
}
|
|
}
|
|
|
|
// Set nonexistent fields in the cache list to false (instead of null)
|
|
for each(var itemID in allItemIDs) {
|
|
for each(var fieldID in fieldIDs) {
|
|
if (Zotero.ItemFields.isValidForType(fieldID, _items[itemID].getType())) {
|
|
if (!itemFieldsCached[itemID] || !itemFieldsCached[itemID][fieldID]) {
|
|
//Zotero.debug('Setting field ' + fieldID + ' to false for item ' + itemID);
|
|
_items[itemID].setField(fieldID, false, true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Delete item(s) from database and clear from internal array
|
|
*
|
|
* If _eraseChildren_ is true, erase child items as well
|
|
**/
|
|
function erase(ids, eraseChildren){
|
|
var unlock = Zotero.Notifier.begin(true);
|
|
Zotero.UnresponsiveScriptIndicator.disable();
|
|
try {
|
|
Zotero.DB.beginTransaction();
|
|
for each(var id in ids) {
|
|
var item = this.get(id);
|
|
if (!item) {
|
|
Zotero.debug('Item ' + id + ' does not exist in Items.erase()!', 1);
|
|
Zotero.Notifier.trigger('delete', 'item', id, [false]);
|
|
continue;
|
|
}
|
|
item.erase(eraseChildren); // calls unload()
|
|
item = undefined;
|
|
}
|
|
this.purge();
|
|
Zotero.DB.commitTransaction();
|
|
}
|
|
catch (e) {
|
|
Zotero.DB.rollbackTransaction();
|
|
throw (e);
|
|
}
|
|
finally {
|
|
Zotero.Notifier.commit(unlock);
|
|
Zotero.UnresponsiveScriptIndicator.enable();
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* Clear entries from various tables that no longer exist
|
|
*
|
|
* This is called automatically by Items.erase() but must be called
|
|
* manually after Item.erase()
|
|
*/
|
|
function purge() {
|
|
Zotero.Creators.purge();
|
|
Zotero.Tags.purge();
|
|
Zotero.Fulltext.purgeUnusedWords();
|
|
// TODO: purge itemDataValues?
|
|
}
|
|
|
|
|
|
/**
|
|
* Clear item from internal array (used by Zotero.Item.erase())
|
|
**/
|
|
function unload(id){
|
|
delete _items[id];
|
|
}
|
|
|
|
|
|
function _load() {
|
|
if (!arguments[0] && _itemsLoaded) {
|
|
return;
|
|
}
|
|
|
|
// Should be the same as parts in Zotero.Item.loadFromID
|
|
var sql = 'SELECT I.itemID, I.itemTypeID, I.dateModified, lastName || '
|
|
+ 'CASE ((SELECT COUNT(*) FROM itemCreators WHERE itemID=I.itemID)>1) '
|
|
+ "WHEN 0 THEN '' ELSE ' et al.' END AS firstCreator, "
|
|
+ "(SELECT COUNT(*) FROM itemNotes WHERE sourceItemID=I.itemID) AS numNotes, "
|
|
+ "(SELECT COUNT(*) FROM itemAttachments WHERE sourceItemID=I.itemID) AS numAttachments "
|
|
+ 'FROM items I '
|
|
+ 'LEFT JOIN itemCreators IC ON (I.itemID=IC.itemID) '
|
|
+ 'LEFT JOIN creators C ON (IC.creatorID=C.creatorID) '
|
|
+ 'WHERE (IC.orderIndex=0 OR IC.orderIndex IS NULL)';
|
|
|
|
if (arguments[0]){
|
|
sql += ' AND I.itemID IN (' + Zotero.join(arguments,',') + ')';
|
|
}
|
|
var itemsRows = Zotero.DB.query(sql);
|
|
|
|
for each(var row in itemsRows) {
|
|
// Item doesn't exist -- create new object and stuff in array
|
|
if (!_items[row['itemID']]){
|
|
var item = new Zotero.Item();
|
|
item.loadFromRow(row, true);
|
|
_items[row['itemID']] = item;
|
|
}
|
|
// Existing item -- reload in place
|
|
else {
|
|
_items[row['itemID']].loadFromRow(row, true);
|
|
}
|
|
}
|
|
|
|
if (!arguments[0]) {
|
|
_itemsLoaded = true;
|
|
_cachedFields = ['itemID', 'itemTypeID', 'dateModified',
|
|
'firstCreator', 'numNotes', 'numAttachments', 'numChildren'];
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
Zotero.Notes = new function(){
|
|
this.add = add;
|
|
this.noteToTitle = noteToTitle;
|
|
|
|
/**
|
|
* Create a new item of type 'note' and add the note text to the itemNotes table
|
|
*
|
|
* Returns the itemID of the new note item
|
|
**/
|
|
function add(text, sourceItemID){
|
|
Zotero.DB.beginTransaction();
|
|
|
|
if (sourceItemID){
|
|
var sourceItem = Zotero.Items.get(sourceItemID);
|
|
if (!sourceItem){
|
|
Zotero.DB.commitTransaction();
|
|
throw ("Cannot set note source to invalid item " + sourceItemID);
|
|
}
|
|
if (!sourceItem.isRegularItem()){
|
|
Zotero.DB.commitTransaction();
|
|
throw ("Cannot set note source to a note or attachment (" + sourceItemID + ")");
|
|
}
|
|
}
|
|
|
|
var note = new Zotero.Item('note');
|
|
note.save();
|
|
|
|
var title = text ? this.noteToTitle(text) : '';
|
|
var sql = "INSERT INTO itemNoteTitles VALUES (?,?)";
|
|
Zotero.DB.query(sql, [note.getID(), title]);
|
|
|
|
var sql = "INSERT INTO itemNotes VALUES (?,?,?)";
|
|
var bindParams = [
|
|
note.getID(),
|
|
(sourceItemID ? {int:sourceItemID} : null),
|
|
{string: text ? text : ''}
|
|
];
|
|
Zotero.DB.query(sql, bindParams);
|
|
Zotero.DB.commitTransaction();
|
|
|
|
// Switch to Zotero.Items version
|
|
var note = Zotero.Items.get(note.getID());
|
|
note.updateNoteCache(text, title);
|
|
|
|
if (sourceItemID){
|
|
var notifierData = { old: sourceItem.toArray() };
|
|
sourceItem.incrementNoteCount();
|
|
Zotero.Notifier.trigger('modify', 'item', sourceItemID, notifierData);
|
|
}
|
|
|
|
return note.getID();
|
|
}
|
|
|
|
|
|
/**
|
|
* Return first line (or first MAX_LENGTH characters) of note content
|
|
**/
|
|
function noteToTitle(text) {
|
|
var MAX_LENGTH = 80;
|
|
|
|
var t = text.substring(0, MAX_LENGTH);
|
|
var ln = t.indexOf("\n");
|
|
if (ln>-1 && ln<MAX_LENGTH){
|
|
t = t.substring(0, ln);
|
|
}
|
|
return t;
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
* Constructor for Collection object
|
|
*
|
|
* Generally should be called from Zotero.Collection rather than directly
|
|
*/
|
|
Zotero.Collection = function(){
|
|
this._init();
|
|
}
|
|
|
|
Zotero.Collection.prototype._init = function(){
|
|
//
|
|
// Public members for access by public methods -- do not access directly
|
|
//
|
|
this._id = null;
|
|
this._name = null;
|
|
this._parent = null;
|
|
this._hasChildCollections = false;
|
|
this._hasChildItems = false;
|
|
this._childItems = new Zotero.Hash();
|
|
this._childItemsLoaded = false;
|
|
}
|
|
|
|
/*
|
|
* Build collection from database
|
|
*/
|
|
Zotero.Collection.prototype.loadFromID = function(id){
|
|
// Should be same as query in Zotero.Collections, just with collectionID
|
|
var sql = "SELECT collectionID, collectionName, parentCollectionID, "
|
|
+ "(SELECT COUNT(*) FROM collections WHERE "
|
|
+ "parentCollectionID=C.collectionID)!=0 AS hasChildCollections, "
|
|
+ "(SELECT COUNT(*) FROM collectionItems WHERE "
|
|
+ "collectionID=C.collectionID)!=0 AS hasChildItems "
|
|
+ "FROM collections C "
|
|
+ "WHERE collectionID=" + id;
|
|
|
|
var row = Zotero.DB.rowQuery(sql);
|
|
this.loadFromRow(row);
|
|
}
|
|
|
|
|
|
/*
|
|
* Populate collection data from a database row
|
|
*/
|
|
Zotero.Collection.prototype.loadFromRow = function(row){
|
|
this._init();
|
|
this._id = row['collectionID'];
|
|
this._name = row['collectionName'];
|
|
this._parent = row['parentCollectionID'];
|
|
this._hasChildCollections = row['hasChildCollections'];
|
|
this._hasChildItems = row['hasChildItems'];
|
|
this._loadChildItems();
|
|
}
|
|
|
|
|
|
Zotero.Collection.prototype.getID = function(){
|
|
return this._id;
|
|
}
|
|
|
|
Zotero.Collection.prototype.getName = function(){
|
|
return this._name;
|
|
}
|
|
|
|
/**
|
|
* Returns collectionID of the parent collection
|
|
**/
|
|
Zotero.Collection.prototype.getParent = function(){
|
|
return this._parent;
|
|
}
|
|
|
|
|
|
Zotero.Collection.prototype.isEmpty = function(){
|
|
return !(parseInt(this._hasChildCollections)) && !(parseInt(this._hasChildItems));
|
|
}
|
|
|
|
Zotero.Collection.prototype.hasChildCollections = function(){
|
|
return !!(parseInt(this._hasChildCollections));
|
|
}
|
|
|
|
Zotero.Collection.prototype.hasChildItems = function(){
|
|
return !!(parseInt(this._hasChildItems));
|
|
}
|
|
|
|
/**
|
|
* Rename the collection
|
|
*
|
|
* _name_ is non-empty string
|
|
*
|
|
* Returns true on success, or false on error
|
|
**/
|
|
Zotero.Collection.prototype.rename = function(name){
|
|
if (!name){
|
|
return false;
|
|
}
|
|
|
|
var notifierData = { old: this.toArray() };
|
|
|
|
var sql = "UPDATE collections SET collectionName=? "
|
|
+ "WHERE collectionID=?";
|
|
Zotero.DB.query(sql, [{'string':name},{'int':this.getID()}]);
|
|
this._name = name;
|
|
|
|
Zotero.Notifier.trigger('modify', 'collection', this.getID(), notifierData);
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Change the parentCollectionID of a collection
|
|
*
|
|
* Returns TRUE on success, FALSE on error
|
|
**/
|
|
Zotero.Collection.prototype.changeParent = function(parent){
|
|
if (!parent){
|
|
parent = null;
|
|
}
|
|
|
|
var previousParent = this.getParent();
|
|
|
|
if (parent==previousParent){
|
|
Zotero.debug('Collection ' + this.getID() + ' is already in '
|
|
+ (parent ? 'collection ' + parent : 'root collection'), 2);
|
|
return false;
|
|
}
|
|
|
|
if (parent && !Zotero.Collections.get(parent)){
|
|
throw('Invalid parentCollectionID ' + parent + ' in changeParent()');
|
|
}
|
|
|
|
if (parent && parent==this.getID()){
|
|
Zotero.debug('Cannot move collection into itself!', 2);
|
|
return false;
|
|
}
|
|
|
|
if (parent){
|
|
if (this.hasDescendent('collection', parent)){
|
|
Zotero.debug('Cannot move collection into one of its own '
|
|
+ 'descendents!', 2);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
var notifierData = { old: this.toArray() };
|
|
|
|
var parentParam = parent ? {'int':parent} : {'null':true};
|
|
|
|
var sql = "UPDATE collections SET parentCollectionID=? "
|
|
+ "WHERE collectionID=?";
|
|
|
|
Zotero.DB.query(sql, [parentParam, {'int':this.getID()}]);
|
|
this._parent = parent;
|
|
|
|
var notifyIDs = [
|
|
this.getID(),
|
|
(previousParent ? previousParent : null),
|
|
(parent ? parent : null)
|
|
];
|
|
|
|
Zotero.Collections.reloadAll();
|
|
|
|
Zotero.Notifier.trigger('move', 'collection', notifyIDs, notifierData);
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Add an item to the collection
|
|
**/
|
|
Zotero.Collection.prototype.addItem = function(itemID){
|
|
Zotero.DB.beginTransaction();
|
|
|
|
if (!Zotero.Items.get(itemID)){
|
|
Zotero.DB.rollbackTransaction();
|
|
throw(itemID + ' is not a valid item id');
|
|
}
|
|
|
|
var nextOrderIndex = Zotero.DB.valueQuery("SELECT IFNULL(MAX(orderIndex)+1, 0) "
|
|
+ "FROM collectionItems WHERE collectionID=" + this._id);
|
|
|
|
var sql = "INSERT OR IGNORE INTO collectionItems VALUES "
|
|
+ "(" + this._id + ", " + itemID + ", " + nextOrderIndex + ")";
|
|
|
|
Zotero.DB.query(sql);
|
|
Zotero.DB.commitTransaction();
|
|
|
|
this._childItems.set(itemID);
|
|
|
|
// If this was previously empty, update and send a notification to the tree
|
|
if (!this._hasChildItems){
|
|
this._hasChildItems = true;
|
|
}
|
|
|
|
Zotero.Notifier.trigger('add', 'collection-item', this.getID() + '-' + itemID);
|
|
}
|
|
|
|
|
|
/**
|
|
* Add multiple items to the collection in batch
|
|
*/
|
|
Zotero.Collection.prototype.addItems = function(itemIDs) {
|
|
if (!itemIDs || !itemIDs.length) {
|
|
return;
|
|
}
|
|
|
|
Zotero.DB.beginTransaction();
|
|
for (var i=0; i<itemIDs.length; i++) {
|
|
this.addItem(itemIDs[i]);
|
|
}
|
|
Zotero.DB.commitTransaction();
|
|
}
|
|
|
|
|
|
/**
|
|
* Remove an item from the collection (does not delete item from library)
|
|
**/
|
|
Zotero.Collection.prototype.removeItem = function(itemID){
|
|
var sql = "DELETE FROM collectionItems WHERE collectionID=" + this._id
|
|
+ " AND itemID=" + itemID;
|
|
Zotero.DB.query(sql);
|
|
|
|
this._childItems.remove(itemID);
|
|
|
|
// If this was the last item, set collection to empty
|
|
if (!this._childItems.length){
|
|
this._hasChildItems = false;
|
|
}
|
|
|
|
Zotero.Notifier.trigger('remove', 'collection-item', this.getID() + '-' + itemID);
|
|
}
|
|
|
|
|
|
/**
|
|
* Remove multiple items from the collection in batch
|
|
* (does not delete item from library)
|
|
*/
|
|
Zotero.Collection.prototype.removeItems = function(itemIDs) {
|
|
if (!itemIDs || !itemIDs.length) {
|
|
return;
|
|
}
|
|
|
|
Zotero.DB.beginTransaction();
|
|
for (var i=0; i<itemIDs.length; i++) {
|
|
this.removeItem(itemIDs[i]);
|
|
}
|
|
Zotero.DB.commitTransaction();
|
|
}
|
|
|
|
/**
|
|
* Check if an item belongs to the collection
|
|
**/
|
|
Zotero.Collection.prototype.hasItem = function(itemID){
|
|
if (!this._childItemsLoaded){
|
|
this._loadChildItems();
|
|
}
|
|
return this._childItems.has(itemID);
|
|
}
|
|
|
|
|
|
Zotero.Collection.prototype.hasDescendent = function(type, id){
|
|
var descendents = this.getDescendents();
|
|
for (var i=0, len=descendents.length; i<len; i++){
|
|
if (descendents[i]['type']==type && descendents[i]['id']==id){
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
/**
|
|
* Deletes collection and all descendent collections and items
|
|
**/
|
|
Zotero.Collection.prototype.erase = function(deleteItems){
|
|
Zotero.DB.beginTransaction();
|
|
|
|
var descendents = this.getDescendents();
|
|
var collections = [this.getID()], items = [];
|
|
var notifierData = [{ old: this.toArray() }];
|
|
|
|
for(var i=0, len=descendents.length; i<len; i++){
|
|
// Descendent collections
|
|
if (descendents[i]['type']=='collection'){
|
|
collections.push(descendents[i]['id']);
|
|
var c = Zotero.Collections.get(descendents[i]['id']);
|
|
notifierData.push(c ? { old: c.toArray() } : false);
|
|
}
|
|
// Descendent items
|
|
else {
|
|
if (deleteItems){
|
|
// Delete items from DB
|
|
Zotero.Items.get(descendents[i]['id']).erase();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove item associations for all descendent collections
|
|
Zotero.DB.query('DELETE FROM collectionItems WHERE collectionID IN ('
|
|
+ collections.join() + ')');
|
|
|
|
// And delete all descendent collections
|
|
Zotero.DB.query('DELETE FROM collections WHERE collectionID IN ('
|
|
+ collections.join() + ')');
|
|
|
|
Zotero.DB.commitTransaction();
|
|
|
|
// Clear deleted collection from internal memory
|
|
Zotero.Collections.unload(collections);
|
|
|
|
Zotero.Collections.reloadAll();
|
|
|
|
Zotero.Notifier.trigger('delete', 'collection', collections, notifierData);
|
|
}
|
|
|
|
|
|
Zotero.Collection.prototype.isCollection = function(){
|
|
return true;
|
|
}
|
|
|
|
|
|
Zotero.Collection.prototype.toArray = function() {
|
|
return {
|
|
id: this.getID(),
|
|
name: this.getName(),
|
|
parent: this.getParent(),
|
|
descendents: this.getDescendents(true)
|
|
};
|
|
}
|
|
|
|
|
|
Zotero.Collection.prototype._loadChildItems = function(){
|
|
this._childItems = new Zotero.Hash();
|
|
|
|
var sql = "SELECT itemID FROM collectionItems WHERE collectionID=" + this._id;
|
|
var itemIDs = Zotero.DB.columnQuery(sql);
|
|
|
|
if (itemIDs){
|
|
for (var i=0; i<itemIDs.length; i++){
|
|
this._childItems.set(itemIDs[i]);
|
|
}
|
|
}
|
|
|
|
this._childItemsLoaded = true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns an array of descendent collections and items
|
|
* (rows of 'id', 'type' ('item' or 'collection'), and, if collection, 'name'
|
|
* and the nesting 'level')
|
|
*
|
|
* nested: Return multidimensional array with 'children' nodes instead of flat array
|
|
**/
|
|
Zotero.Collection.prototype.getDescendents = function(nested, type, level){
|
|
var toReturn = new Array();
|
|
|
|
if (!level) {
|
|
level = 1;
|
|
}
|
|
|
|
// 0 == collection
|
|
// 1 == item
|
|
var children = Zotero.DB.query('SELECT collectionID AS id, '
|
|
+ "0 AS type, collectionName AS collectionName "
|
|
+ 'FROM collections WHERE parentCollectionID=' + this._id
|
|
+ ' UNION SELECT itemID AS id, 1 AS type, NULL AS collectionName '
|
|
+ 'FROM collectionItems WHERE collectionID=' + this._id);
|
|
|
|
if (type){
|
|
switch (type){
|
|
case 'item':
|
|
case 'collection':
|
|
break;
|
|
default:
|
|
throw ("Invalid type '" + type + "' in Collection.getDescendents()");
|
|
}
|
|
}
|
|
|
|
for(var i=0, len=children.length; i<len; i++){
|
|
// This seems to not work without parseInt() even though
|
|
// typeof children[i]['type'] == 'number' and
|
|
// children[i]['type'] === parseInt(children[i]['type']),
|
|
// which sure seems like a bug to me
|
|
switch (parseInt(children[i]['type'])){
|
|
case 0:
|
|
if (!type || type=='collection'){
|
|
toReturn.push({
|
|
id: children[i]['id'],
|
|
name: children[i]['collectionName'],
|
|
type: 'collection',
|
|
level: level
|
|
});
|
|
}
|
|
|
|
var descendents =
|
|
Zotero.Collections.get(children[i]['id']).getDescendents(nested, type, level+1);
|
|
|
|
if (nested){
|
|
toReturn[toReturn.length-1]['children'] = descendents;
|
|
}
|
|
else {
|
|
for(var j=0, len2=descendents.length; j<len2; j++){
|
|
toReturn.push(descendents[j]);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 1:
|
|
if (!type || type=='item'){
|
|
toReturn.push({
|
|
id: children[i]['id'],
|
|
type: 'item'
|
|
});
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
return toReturn;
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
* Primary interface for accessing Zotero collection
|
|
*/
|
|
Zotero.Collections = new function(){
|
|
var _collections = new Array();
|
|
var _collectionsLoaded = false;
|
|
|
|
this.get = get;
|
|
this.add = add;
|
|
this.getCollectionsContainingItems = getCollectionsContainingItems;
|
|
this.reloadAll = reloadAll;
|
|
this.unload = unload;
|
|
|
|
/*
|
|
* Returns a Zotero.Collection object for a collectionID
|
|
*/
|
|
function get(id){
|
|
if (!_collectionsLoaded){
|
|
this.reloadAll();
|
|
}
|
|
return (typeof _collections[id]!='undefined') ? _collections[id] : false;
|
|
}
|
|
|
|
|
|
/**
|
|
* Add new collection to DB and return Collection object
|
|
*
|
|
* _name_ is non-empty string
|
|
* _parent_ is optional collectionID -- creates root collection by default
|
|
*
|
|
* Returns true on success; false on error
|
|
**/
|
|
function add(name, parent){
|
|
if (!name){
|
|
return false;
|
|
}
|
|
|
|
Zotero.DB.beginTransaction();
|
|
|
|
if (parent && !this.get(parent)){
|
|
Zotero.DB.rollbackTransaction();
|
|
throw('Cannot add collection to invalid parent ' + parent);
|
|
}
|
|
|
|
var parentParam = parent ? {'int':parent} : {'null':true};
|
|
|
|
var rnd = Zotero.getRandomID('collections', 'collectionID');
|
|
|
|
var sql = "INSERT INTO collections VALUES (?,?,?)";
|
|
var sqlValues = [ {'int':rnd}, {'string':name}, parentParam ];
|
|
Zotero.DB.query(sql, sqlValues);
|
|
|
|
Zotero.DB.commitTransaction();
|
|
|
|
this.reloadAll();
|
|
|
|
Zotero.Notifier.trigger('add', 'collection', rnd);
|
|
|
|
return this.get(rnd);
|
|
}
|
|
|
|
|
|
function getCollectionsContainingItems(itemIDs, asIDs) {
|
|
var sql = "SELECT collectionID FROM collections WHERE ";
|
|
var sqlParams = [];
|
|
for each(var id in itemIDs) {
|
|
sql += "collectionID IN (SELECT collectionID FROM collectionItems "
|
|
+ "WHERE itemID=?) AND "
|
|
sqlParams.push(id);
|
|
}
|
|
sql = sql.substring(0, sql.length - 5);
|
|
var collectionIDs = Zotero.DB.columnQuery(sql, sqlParams);
|
|
|
|
if (asIDs) {
|
|
return collectionIDs;
|
|
}
|
|
|
|
return Zotero.Collections.get(collectionIDs);
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Clear collection from internal cache (used by Zotero.Collection.erase())
|
|
*
|
|
* Can be passed ids as individual parameters or as an array of ids, or both
|
|
**/
|
|
function unload(){
|
|
var ids = Zotero.flattenArguments(arguments);
|
|
|
|
for(var i=0; i<ids.length; i++){
|
|
delete _collections[ids[i]];
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Loads collection data from DB and adds to internal cache
|
|
**/
|
|
function reloadAll() {
|
|
// This should be the same as the query in Zotero.Collection.loadFromID,
|
|
// just without a specific collectionID
|
|
var sql = "SELECT collectionID, collectionName, parentCollectionID, "
|
|
+ "(SELECT COUNT(*) FROM collections WHERE "
|
|
+ "parentCollectionID=C.collectionID)!=0 AS hasChildCollections, "
|
|
+ "(SELECT COUNT(*) FROM collectionItems WHERE "
|
|
+ "collectionID=C.collectionID)!=0 AS hasChildItems "
|
|
+ "FROM collections C";
|
|
|
|
var ids = Zotero.flattenArguments(arguments)
|
|
if (ids.length){
|
|
sql += " WHERE collectionID IN (" + ids.join() + ")";
|
|
}
|
|
|
|
var result = Zotero.DB.query(sql);
|
|
|
|
var collectionIDs = [];
|
|
|
|
if (result){
|
|
for (var i=0; i<result.length; i++){
|
|
var collectionID = result[i]['collectionID'];
|
|
collectionIDs.push(collectionID);
|
|
|
|
// If collection doesn't exist, create new object and stuff in array
|
|
if (!_collections[collectionID]){
|
|
var collection = new Zotero.Collection();
|
|
collection.loadFromRow(result[i]);
|
|
_collections[collectionID] = collection;
|
|
}
|
|
// If existing collection, reload in place
|
|
else {
|
|
_collections[collectionID].loadFromRow(result[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove old collections that no longer exist
|
|
for each(var c in _collections) {
|
|
if (collectionIDs.indexOf(c.getID()) == -1) {
|
|
this.unload(c.getID());
|
|
}
|
|
}
|
|
|
|
_collectionsLoaded = true;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
* Same structure as Zotero.Tags -- make changes in both places if possible
|
|
*/
|
|
Zotero.Creators = new function(){
|
|
var _creators = new Array; // indexed by first%%%last%%%fieldMode hash
|
|
var _creatorsByID = new Array; // indexed by creatorID
|
|
|
|
this.get = get;
|
|
this.getID = getID;
|
|
this.add = add;
|
|
this.purge = purge;
|
|
|
|
var self = this;
|
|
|
|
/*
|
|
* Returns an array of creator data for a given creatorID
|
|
*/
|
|
function get(creatorID){
|
|
if (_creatorsByID[creatorID]){
|
|
return _creatorsByID[creatorID];
|
|
}
|
|
|
|
var sql = 'SELECT * FROM creators WHERE creatorID=' + creatorID;
|
|
var result = Zotero.DB.rowQuery(sql);
|
|
|
|
if (!result){
|
|
return false;
|
|
}
|
|
|
|
_creatorsByID[creatorID] = result;
|
|
return result;
|
|
}
|
|
|
|
|
|
/*
|
|
* Returns the creatorID matching given name and type
|
|
*/
|
|
function getID(firstName, lastName, fieldMode){
|
|
if (!firstName){
|
|
firstName = '';
|
|
}
|
|
if (!lastName){
|
|
lastName = '';
|
|
}
|
|
|
|
// Only two modes for now
|
|
if (fieldMode){
|
|
firstName = '';
|
|
fieldMode = 1;
|
|
}
|
|
else {
|
|
fieldMode = 0;
|
|
}
|
|
|
|
var hash = firstName + '%%%' + lastName + '%%%' + fieldMode;
|
|
|
|
if (_creators[hash]){
|
|
return _creators[hash];
|
|
}
|
|
|
|
var sql = 'SELECT creatorID FROM creators '
|
|
+ 'WHERE firstName=? AND lastName=? AND fieldMode=?';
|
|
var params = [{string: firstName}, {string: lastName}, fieldMode];
|
|
var creatorID = Zotero.DB.valueQuery(sql, params);
|
|
|
|
if (creatorID){
|
|
_creators[hash] = creatorID;
|
|
}
|
|
|
|
return creatorID;
|
|
}
|
|
|
|
|
|
/*
|
|
* Add a new creator to the database
|
|
*
|
|
* Returns new creatorID
|
|
*/
|
|
function add(firstName, lastName, fieldMode){
|
|
Zotero.debug('Adding new creator', 4);
|
|
|
|
Zotero.DB.beginTransaction();
|
|
|
|
var sql = 'INSERT INTO creators VALUES (?,?,?,?)';
|
|
var rnd = Zotero.getRandomID('creators', 'creatorID');
|
|
var params = [
|
|
rnd, fieldMode ? '' : {string: firstName}, {string: lastName},
|
|
fieldMode ? 1 : 0
|
|
];
|
|
Zotero.DB.query(sql, params);
|
|
|
|
Zotero.DB.commitTransaction();
|
|
return rnd;
|
|
}
|
|
|
|
|
|
/*
|
|
* Delete obsolete creators from database and clear internal array entries
|
|
*
|
|
* Returns removed creatorIDs on success
|
|
*/
|
|
function purge(){
|
|
var sql = 'SELECT creatorID FROM creators WHERE creatorID NOT IN '
|
|
+ '(SELECT creatorID FROM itemCreators);';
|
|
var toDelete = Zotero.DB.columnQuery(sql);
|
|
|
|
if (!toDelete){
|
|
return false;
|
|
}
|
|
|
|
// Clear creator entries in internal array
|
|
for (var i=0; i<toDelete.length; i++){
|
|
var hash = _getHash(toDelete[i]);
|
|
delete _creators[hash];
|
|
delete _creatorsByID[toDelete[i]];
|
|
}
|
|
|
|
sql = 'DELETE FROM creators WHERE creatorID NOT IN '
|
|
+ '(SELECT creatorID FROM itemCreators);';
|
|
var result = Zotero.DB.query(sql);
|
|
|
|
return toDelete;
|
|
}
|
|
|
|
|
|
function _getHash(creatorID){
|
|
var creator = self.get(creatorID);
|
|
if (!creator){
|
|
return false;
|
|
}
|
|
return creator['firstName'] + '%%%' + creator['lastName'] + '%%%' +
|
|
creator['fieldMode'];
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* Same structure as Zotero.Creators -- make changes in both places if possible
|
|
*/
|
|
Zotero.Tags = new function(){
|
|
var _tags = []; // indexed by tag text
|
|
var _tagsByID = []; // indexed by tagID
|
|
|
|
this.get = get;
|
|
this.getName = getName;
|
|
this.getID = getID;
|
|
this.getIDs = getIDs;
|
|
this.getTypes = getTypes;
|
|
this.getAll = getAll;
|
|
this.getAllWithinSearch = getAllWithinSearch;
|
|
this.getTagItems = getTagItems;
|
|
this.search = search;
|
|
this.add = add;
|
|
this.rename = rename;
|
|
this.remove = remove;
|
|
this.purge = purge;
|
|
this.toArray = toArray;
|
|
|
|
|
|
/*
|
|
* Returns a tag and type for a given tagID
|
|
*/
|
|
function get(tagID) {
|
|
if (_tagsByID[tagID]){
|
|
return _tagsByID[tagID];
|
|
}
|
|
|
|
var sql = 'SELECT tag, tagType FROM tags WHERE tagID=?';
|
|
var result = Zotero.DB.rowQuery(sql, tagID);
|
|
|
|
if (!result){
|
|
return false;
|
|
}
|
|
|
|
_tagsByID[tagID] = {
|
|
tag: result.tag,
|
|
type: result.tagType
|
|
};
|
|
return result;
|
|
}
|
|
|
|
|
|
/*
|
|
* Returns a tag for a given tagID
|
|
*/
|
|
function getName(tagID) {
|
|
if (_tagsByID[tagID]){
|
|
return _tagsByID[tagID].tag;
|
|
}
|
|
|
|
var tag = this.get(tagID);
|
|
|
|
return _tagsByID[tagID] ? _tagsByID[tagID].tag : false;
|
|
}
|
|
|
|
|
|
/*
|
|
* Returns the tagID matching given tag and type
|
|
*/
|
|
function getID(tag, type) {
|
|
if (_tags[type] && _tags[type]['_' + tag]){
|
|
return _tags[type]['_' + tag];
|
|
}
|
|
|
|
var sql = 'SELECT tagID FROM tags WHERE tag=? AND tagType=?';
|
|
var tagID = Zotero.DB.valueQuery(sql, [tag, type]);
|
|
|
|
if (tagID) {
|
|
if (!_tags[type]) {
|
|
_tags[type] = [];
|
|
}
|
|
_tags[type]['_' + tag] = tagID;
|
|
}
|
|
|
|
return tagID;
|
|
}
|
|
|
|
|
|
/*
|
|
* Returns all tagIDs for this tag (of all types)
|
|
*/
|
|
function getIDs(tag) {
|
|
var sql = 'SELECT tagID FROM tags WHERE tag=?';
|
|
return Zotero.DB.columnQuery(sql, [tag]);
|
|
}
|
|
|
|
|
|
/*
|
|
* Returns an array of tagTypes for tags matching given tag
|
|
*/
|
|
function getTypes(tag) {
|
|
var sql = 'SELECT tagType FROM tags WHERE tag=?';
|
|
return Zotero.DB.columnQuery(sql, [tag]);
|
|
}
|
|
|
|
|
|
/**
|
|
* Get all tags indexed by tagID
|
|
*
|
|
* _types_ is an optional array of tagTypes to fetch
|
|
*/
|
|
function getAll(types) {
|
|
var sql = "SELECT tagID, tag, tagType FROM tags ";
|
|
if (types) {
|
|
sql += "WHERE tagType IN (" + types.join() + ") ";
|
|
}
|
|
sql += "ORDER BY tag COLLATE NOCASE";
|
|
var tags = Zotero.DB.query(sql);
|
|
var indexed = {};
|
|
for each(var tag in tags) {
|
|
indexed[tag.tagID] = {
|
|
tag: tag.tag,
|
|
type: tag.tagType
|
|
};
|
|
}
|
|
return indexed;
|
|
}
|
|
|
|
|
|
/*
|
|
* Get all tags within the items of a Zotero.Search object
|
|
*
|
|
* _types_ is an optional array of tagTypes to fetch
|
|
*/
|
|
function getAllWithinSearch(search, types) {
|
|
// If search has post-search filters (e.g. fulltext content), run it
|
|
// and just include the ids
|
|
//
|
|
// DEBUG: it's possible there's a query length limit here
|
|
// or that this slows things down with large libraries
|
|
// -- should probably use a temporary table instead
|
|
if (search.hasPostSearchFilter()) {
|
|
var ids = search.search();
|
|
var sql = "SELECT DISTINCT tagID, tag, tagType FROM itemTags "
|
|
+ "NATURAL JOIN tags WHERE itemID IN (" + ids.join() + ") ";
|
|
if (types) {
|
|
sql += "AND tagType IN (" + types.join() + ") ";
|
|
}
|
|
sql += "ORDER BY tag COLLATE NOCASE";
|
|
var tags = Zotero.DB.query(sql);
|
|
}
|
|
else {
|
|
var sql = "SELECT DISTINCT tagID, tag, tagType FROM itemTags "
|
|
+ "NATURAL JOIN tags WHERE itemID IN (" + search.getSQL() + ") ";
|
|
if (types) {
|
|
sql += "AND tagType IN (" + types.join() + ") ";
|
|
}
|
|
sql += "ORDER BY tag COLLATE NOCASE";
|
|
var tags = Zotero.DB.query(sql, search.getSQLParams());
|
|
}
|
|
|
|
var indexed = {};
|
|
for each(var tag in tags) {
|
|
indexed[tag.tagID] = {
|
|
tag: tag.tag,
|
|
type: tag.tagType
|
|
};
|
|
}
|
|
return indexed;
|
|
}
|
|
|
|
|
|
function getTagItems(tagID) {
|
|
var sql = "SELECT itemID FROM itemTags WHERE tagID=?";
|
|
return Zotero.DB.columnQuery(sql, tagID);
|
|
}
|
|
|
|
|
|
function search(str){
|
|
var sql = 'SELECT tagID, tag, tagType FROM tags';
|
|
if (str) {
|
|
sql += ' WHERE tag LIKE ?';
|
|
}
|
|
sql += ' ORDER BY tag COLLATE NOCASE';
|
|
var tags = Zotero.DB.query(sql, str ? '%' + str + '%' : undefined);
|
|
var indexed = {};
|
|
for each(var tag in tags) {
|
|
indexed[tag.tagID] = {
|
|
tag: tag.tag,
|
|
type: tag.tagType
|
|
};
|
|
}
|
|
return indexed;
|
|
}
|
|
|
|
|
|
/*
|
|
* Add a new tag to the database
|
|
*
|
|
* Returns new tagID
|
|
*/
|
|
function add(tag, type){
|
|
if (type != 0 && type != 1) {
|
|
throw ('Invalid tag type ' + type + ' in Tags.add()');
|
|
}
|
|
|
|
if (!type) {
|
|
type = 0;
|
|
}
|
|
|
|
Zotero.debug('Adding new tag of type ' + type, 4);
|
|
|
|
Zotero.DB.beginTransaction();
|
|
|
|
var sql = 'INSERT INTO tags VALUES (?,?,?)';
|
|
var rnd = Zotero.getRandomID('tags', 'tagID');
|
|
Zotero.DB.query(sql, [{int: rnd}, {string: tag}, {int: type}]);
|
|
|
|
Zotero.DB.commitTransaction();
|
|
Zotero.Notifier.trigger('add', 'tag', rnd);
|
|
return rnd;
|
|
}
|
|
|
|
|
|
function rename(tagID, tag) {
|
|
Zotero.debug('Renaming tag', 4);
|
|
|
|
Zotero.DB.beginTransaction();
|
|
|
|
var tagObj = this.get(tagID);
|
|
var oldName = tagObj.tag;
|
|
var oldType = tagObj.type;
|
|
var notifierData = [{ old: this.toArray() }];
|
|
|
|
if (oldName == tag) {
|
|
if (oldType != 0) {
|
|
var sql = "UPDATE tags SET tagType=0 WHERE tagID=?";
|
|
Zotero.DB.query(sql, tagID);
|
|
Zotero.Notifier.trigger('modify', 'tag', tagID, notifierData);
|
|
}
|
|
Zotero.DB.commitTransaction();
|
|
return;
|
|
}
|
|
|
|
// Check if the new tag already exists
|
|
var sql = "SELECT tagID FROM tags WHERE tag=? AND tagType=0";
|
|
var existingTagID = Zotero.DB.valueQuery(sql, tag);
|
|
if (existingTagID) {
|
|
var itemIDs = this.getTagItems(tagID);
|
|
var existingItemIDs = this.getTagItems(existingTagID);
|
|
|
|
// Would be easier to just call removeTag(tagID) and addTag(existingID)
|
|
// here, but this is considerably more efficient
|
|
var sql = "UPDATE OR REPLACE itemTags SET tagID=? WHERE tagID=?";
|
|
Zotero.DB.query(sql, [existingTagID, tagID]);
|
|
|
|
// Manual purge of old tag
|
|
var sql = "DELETE FROM tags WHERE tagID=?";
|
|
Zotero.DB.query(sql, tagID);
|
|
if (_tags[oldType]) {
|
|
delete _tags[oldType]['_' + oldName];
|
|
}
|
|
delete _tagsByID[tagID];
|
|
Zotero.Notifier.trigger('delete', 'tag', tagID, notifierData);
|
|
|
|
// Simulate tag removal on items that used old tag
|
|
var itemTags = [];
|
|
for (var i in itemIDs) {
|
|
itemTags.push(itemIDs[i] + '-' + tagID);
|
|
}
|
|
Zotero.Notifier.trigger('remove', 'item-tag', itemTags);
|
|
|
|
// And send tag add for new tag (except for those that already had it)
|
|
var itemTags = [];
|
|
for (var i in itemIDs) {
|
|
if (existingItemIDs.indexOf(itemIDs[i]) == -1) {
|
|
itemTags.push(itemIDs[i] + '-' + existingTagID);
|
|
}
|
|
}
|
|
Zotero.Notifier.trigger('add', 'item-tag', itemTags);
|
|
|
|
Zotero.Notifier.trigger('modify', 'item', itemIDs);
|
|
Zotero.DB.commitTransaction();
|
|
return;
|
|
}
|
|
|
|
// 0 == user tag -- we set all renamed tags to 0
|
|
var sql = "UPDATE tags SET tag=?, tagType=0 WHERE tagID=?";
|
|
Zotero.DB.query(sql, [{string: tag}, tagID]);
|
|
|
|
var itemIDs = this.getTagItems(tagID);
|
|
|
|
if (_tags[oldType]) {
|
|
delete _tags[oldType]['_' + oldName];
|
|
}
|
|
delete _tagsByID[tagID];
|
|
|
|
Zotero.DB.commitTransaction();
|
|
|
|
Zotero.Notifier.trigger('modify', 'item', itemIDs);
|
|
Zotero.Notifier.trigger('modify', 'tag', tagID, notifierData);
|
|
}
|
|
|
|
|
|
function remove(tagID) {
|
|
Zotero.DB.beginTransaction();
|
|
|
|
var sql = "SELECT itemID FROM itemTags WHERE tagID=?";
|
|
var itemIDs = Zotero.DB.columnQuery(sql, tagID);
|
|
|
|
if (!itemIDs) {
|
|
Zotero.DB.commitTransaction();
|
|
return;
|
|
}
|
|
|
|
var sql = "DELETE FROM itemTags WHERE tagID=?";
|
|
Zotero.DB.query(sql, tagID);
|
|
|
|
Zotero.Notifier.trigger('modify', 'item', itemIDs)
|
|
var itemTags = [];
|
|
for (var i in itemIDs) {
|
|
itemTags.push(itemIDs[i] + '-' + tagID);
|
|
}
|
|
Zotero.Notifier.trigger('remove', 'item-tag', itemTags);
|
|
|
|
this.purge();
|
|
Zotero.DB.commitTransaction();
|
|
return;
|
|
}
|
|
|
|
|
|
/*
|
|
* Delete obsolete tags from database and clear internal array entries
|
|
*
|
|
* Returns removed tagIDs on success
|
|
*/
|
|
function purge(){
|
|
Zotero.DB.beginTransaction();
|
|
|
|
var sql = 'SELECT tagID, tag, tagType FROM tags WHERE tagID '
|
|
+ 'NOT IN (SELECT tagID FROM itemTags);';
|
|
var toDelete = Zotero.DB.query(sql);
|
|
|
|
if (!toDelete){
|
|
Zotero.DB.commitTransaction();
|
|
return false;
|
|
}
|
|
|
|
var purged = [];
|
|
var notifierData = [];
|
|
|
|
// Clear tag entries in internal array
|
|
for each(var tag in toDelete) {
|
|
notifierData.push(Zotero.Tags.toArray(tag.tagID));
|
|
|
|
purged.push(tag.tagID);
|
|
if (_tags[tag.tagType]) {
|
|
delete _tags[tag.tagType]['_' + tag.tag];
|
|
}
|
|
delete _tagsByID[tag.tagID];
|
|
}
|
|
|
|
sql = 'DELETE FROM tags WHERE tagID NOT IN '
|
|
+ '(SELECT tagID FROM itemTags);';
|
|
var result = Zotero.DB.query(sql);
|
|
|
|
Zotero.DB.commitTransaction();
|
|
|
|
Zotero.Notifier.trigger('delete', 'tag', purged, notifierData);
|
|
|
|
return toDelete;
|
|
}
|
|
|
|
|
|
function toArray(tagID) {
|
|
var obj = this.get(tagID);
|
|
obj.id = tagID;
|
|
return obj;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
* Base function for retrieving ids and names of static types stored in the DB
|
|
* (e.g. creatorType, fileType, charset, itemType)
|
|
*
|
|
* Extend using the following code within a child constructor:
|
|
*
|
|
* Zotero.CachedTypes.apply(this, arguments);
|
|
* this.constructor.prototype = new Zotero.CachedTypes();
|
|
*
|
|
* And the following properties:
|
|
*
|
|
* this._typeDesc = 'c';
|
|
* this._idCol = '';
|
|
* this._nameCol = '';
|
|
* this._table = '';
|
|
* this._ignoreCase = false;
|
|
*
|
|
*/
|
|
Zotero.CachedTypes = function(){
|
|
var _types = [];
|
|
var _typesLoaded;
|
|
var self = this;
|
|
|
|
// Override these variables in child classes
|
|
this._typeDesc = '';
|
|
this._idCol = '';
|
|
this._nameCol = '';
|
|
this._table = '';
|
|
this._ignoreCase = false;
|
|
|
|
this.getName = getName;
|
|
this.getID = getID;
|
|
this.getTypes = getTypes;
|
|
|
|
function getName(idOrName){
|
|
if (!_typesLoaded){
|
|
_load();
|
|
}
|
|
|
|
if (this._ignoreCase){
|
|
idOrName = idOrName + '';
|
|
idOrName = idOrName.toLowerCase();
|
|
}
|
|
|
|
if (!_types['_' + idOrName]){
|
|
Zotero.debug('Invalid ' + this._typeDesc + ' ' + idOrName, 1);
|
|
return '';
|
|
}
|
|
|
|
return _types['_' + idOrName]['name'];
|
|
}
|
|
|
|
|
|
function getID(idOrName){
|
|
if (!_typesLoaded){
|
|
_load();
|
|
}
|
|
|
|
if (this._ignoreCase){
|
|
idOrName = idOrName + '';
|
|
idOrName = idOrName.toLowerCase();
|
|
}
|
|
|
|
if (!_types['_' + idOrName]){
|
|
Zotero.debug('Invalid ' + this._typeDesc + ' ' + idOrName, 1);
|
|
return false;
|
|
}
|
|
|
|
return _types['_' + idOrName]['id'];
|
|
}
|
|
|
|
|
|
function getTypes(where){
|
|
return Zotero.DB.query('SELECT ' + this._idCol + ' AS id, '
|
|
+ this._nameCol + ' AS name FROM ' + this._table
|
|
+ (where ? ' ' + where : '') + ' ORDER BY ' + this._nameCol);
|
|
}
|
|
|
|
|
|
function _load(){
|
|
var types = self.getTypes();
|
|
|
|
for (i in types){
|
|
// Store as both id and name for access by either
|
|
var typeData = {
|
|
id: types[i]['id'],
|
|
name: types[i]['name']
|
|
}
|
|
_types['_' + types[i]['id']] = typeData;
|
|
if (self._ignoreCase){
|
|
_types['_' + types[i]['name'].toLowerCase()] = _types['_' + types[i]['id']];
|
|
}
|
|
else {
|
|
_types['_' + types[i]['name']] = _types['_' + types[i]['id']];
|
|
}
|
|
}
|
|
|
|
_typesLoaded = true;
|
|
}
|
|
}
|
|
|
|
|
|
Zotero.CreatorTypes = new function(){
|
|
Zotero.CachedTypes.apply(this, arguments);
|
|
this.constructor.prototype = new Zotero.CachedTypes();
|
|
|
|
this.getTypesForItemType = getTypesForItemType;
|
|
this.isValidForItemType = isValidForItemType;
|
|
this.getPrimaryIDForType = getPrimaryIDForType;
|
|
|
|
this._typeDesc = 'creator type';
|
|
this._idCol = 'creatorTypeID';
|
|
this._nameCol = 'creatorType';
|
|
this._table = 'creatorTypes';
|
|
|
|
function getTypesForItemType(itemTypeID){
|
|
var sql = "SELECT creatorTypeID AS id, creatorType AS name "
|
|
+ "FROM itemTypeCreatorTypes NATURAL JOIN creatorTypes "
|
|
// DEBUG: sort needs to be on localized strings in itemPane.js
|
|
// (though still put primary field at top)
|
|
+ "WHERE itemTypeID=? ORDER BY primaryField=1 DESC, name";
|
|
return Zotero.DB.query(sql, itemTypeID);
|
|
}
|
|
|
|
|
|
function isValidForItemType(creatorTypeID, itemTypeID){
|
|
var sql = "SELECT COUNT(*) FROM itemTypeCreatorTypes "
|
|
+ "WHERE itemTypeID=? AND creatorTypeID=?";
|
|
return !!Zotero.DB.valueQuery(sql, [itemTypeID, creatorTypeID]);
|
|
}
|
|
|
|
|
|
function getPrimaryIDForType(itemTypeID){
|
|
var sql = "SELECT creatorTypeID FROM itemTypeCreatorTypes "
|
|
+ "WHERE itemTypeID=? AND primaryField=1";
|
|
return Zotero.DB.valueQuery(sql, itemTypeID);
|
|
}
|
|
}
|
|
|
|
|
|
Zotero.ItemTypes = new function(){
|
|
Zotero.CachedTypes.apply(this, arguments);
|
|
this.constructor.prototype = new Zotero.CachedTypes();
|
|
|
|
this.getPrimaryTypes = getPrimaryTypes;
|
|
this.getSecondaryTypes = getSecondaryTypes;
|
|
this.getHiddenTypes = getHiddenTypes;
|
|
this.getImageSrc = getImageSrc;
|
|
|
|
this._typeDesc = 'item type';
|
|
this._idCol = 'itemTypeID';
|
|
this._nameCol = 'typeName';
|
|
this._table = 'itemTypes';
|
|
|
|
function getPrimaryTypes(){
|
|
return this.getTypes('WHERE display=2');
|
|
}
|
|
|
|
function getSecondaryTypes(){
|
|
return this.getTypes('WHERE display=1');
|
|
}
|
|
|
|
function getHiddenTypes(){
|
|
return this.getTypes('WHERE display=0');
|
|
}
|
|
|
|
function getImageSrc(itemType) {
|
|
// DEBUG: only have icons for some types so far
|
|
switch (itemType) {
|
|
case 'attachment-file':
|
|
case 'attachment-link':
|
|
case 'attachment-snapshot':
|
|
case 'attachment-web-link':
|
|
case 'artwork':
|
|
case 'audioRecording':
|
|
case 'blogPost':
|
|
case 'book':
|
|
case 'bookSection':
|
|
case 'computerProgram':
|
|
case 'conferencePaper':
|
|
case 'email':
|
|
case 'film':
|
|
case 'forumPost':
|
|
case 'interview':
|
|
case 'journalArticle':
|
|
case 'letter':
|
|
case 'magazineArticle':
|
|
case 'manuscript':
|
|
case 'map':
|
|
case 'newspaperArticle':
|
|
case 'note':
|
|
case 'podcast':
|
|
case 'radioBroadcast':
|
|
case 'report':
|
|
case 'thesis':
|
|
case 'tvBroadcast':
|
|
case 'videoRecording':
|
|
case 'webpage':
|
|
|
|
return "chrome://zotero/skin/treeitem-" + itemType + ".png";
|
|
}
|
|
|
|
return "chrome://zotero/skin/treeitem.png";
|
|
}
|
|
}
|
|
|
|
|
|
Zotero.FileTypes = new function(){
|
|
Zotero.CachedTypes.apply(this, arguments);
|
|
this.constructor.prototype = new Zotero.CachedTypes();
|
|
|
|
this._typeDesc = 'file type';
|
|
this._idCol = 'fileTypeID';
|
|
this._nameCol = 'fileType';
|
|
this._table = 'fileTypes';
|
|
|
|
this.getIDFromMIMEType = getIDFromMIMEType;
|
|
|
|
function getIDFromMIMEType(mimeType){
|
|
var sql = "SELECT fileTypeID FROM fileTypeMIMETypes "
|
|
+ "WHERE ? LIKE mimeType || '%'";
|
|
|
|
return Zotero.DB.valueQuery(sql, [mimeType]);
|
|
}
|
|
}
|
|
|
|
|
|
Zotero.CharacterSets = new function(){
|
|
Zotero.CachedTypes.apply(this, arguments);
|
|
this.constructor.prototype = new Zotero.CachedTypes();
|
|
|
|
this._typeDesc = 'character sets';
|
|
this._idCol = 'charsetID';
|
|
this._nameCol = 'charset';
|
|
this._table = 'charsets';
|
|
this._ignoreCase = true;
|
|
|
|
this.getAll = getAll;
|
|
|
|
function getAll(){
|
|
return this.getTypes();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
Zotero.ItemFields = new function(){
|
|
// Private members
|
|
var _fields = {};
|
|
var _fieldsLoaded;
|
|
var _fieldFormats = [];
|
|
var _itemTypeFields = [];
|
|
var _baseTypeFields = [];
|
|
var _typeFieldIDsByBase = {};
|
|
var _typeFieldNamesByBase = {};
|
|
|
|
var self = this;
|
|
|
|
// Privileged methods
|
|
this.getName = getName;
|
|
this.getID = getID;
|
|
this.getLocalizedString = getLocalizedString;
|
|
this.isValidForType = isValidForType;
|
|
this.isInteger = isInteger;
|
|
this.getItemTypeFields = getItemTypeFields;
|
|
this.isBaseField = isBaseField;
|
|
this.isFieldOfBase = isFieldOfBase;
|
|
this.getBaseMappedFields = getBaseMappedFields;
|
|
this.getFieldIDFromTypeAndBase = getFieldIDFromTypeAndBase;
|
|
this.getBaseIDFromTypeAndField = getBaseIDFromTypeAndField;
|
|
this.getTypeFieldsFromBase = getTypeFieldsFromBase;
|
|
|
|
|
|
/*
|
|
* Return the fieldID for a passed fieldID or fieldName
|
|
*/
|
|
function getID(field){
|
|
if (!_fieldsLoaded){
|
|
_loadFields();
|
|
}
|
|
|
|
if (typeof field == 'number') {
|
|
return field;
|
|
}
|
|
|
|
return _fields[field] ? _fields[field]['id'] : false;
|
|
}
|
|
|
|
|
|
/*
|
|
* Return the fieldName for a passed fieldID or fieldName
|
|
*/
|
|
function getName(field){
|
|
if (!_fieldsLoaded){
|
|
_loadFields();
|
|
}
|
|
|
|
return _fields[field] ? _fields[field]['name'] : false;
|
|
}
|
|
|
|
|
|
function getLocalizedString(itemTypeID, field) {
|
|
var fieldName = this.getName(field);
|
|
|
|
// Fields in items are special cases
|
|
switch (field) {
|
|
case 'dateAdded':
|
|
case 'dateModified':
|
|
fieldName = field;
|
|
}
|
|
|
|
// TODO: different labels for different item types
|
|
|
|
return Zotero.getString("itemFields." + fieldName);
|
|
}
|
|
|
|
|
|
function isValidForType(fieldID, itemTypeID){
|
|
if (!_fieldsLoaded){
|
|
_loadFields();
|
|
}
|
|
|
|
_fieldCheck(fieldID, 'isValidForType');
|
|
|
|
if (!_fields[fieldID]['itemTypes']){
|
|
return false;
|
|
}
|
|
|
|
return !!_fields[fieldID]['itemTypes'][itemTypeID];
|
|
}
|
|
|
|
|
|
function isInteger(fieldID){
|
|
if (!_fieldsLoaded){
|
|
_loadFields();
|
|
}
|
|
|
|
_fieldCheck(fieldID, 'isInteger');
|
|
|
|
var ffid = _fields[fieldID]['formatID'];
|
|
return _fieldFormats[ffid] ? _fieldFormats[ffid]['isInteger'] : false;
|
|
}
|
|
|
|
|
|
/*
|
|
* Returns an array of fieldIDs for a given item type
|
|
*/
|
|
function getItemTypeFields(itemTypeID){
|
|
if (_itemTypeFields[itemTypeID]){
|
|
return _itemTypeFields[itemTypeID];
|
|
}
|
|
|
|
if (!itemTypeID){
|
|
throw("Invalid item type id '" + itemTypeID
|
|
+ "' provided to getItemTypeFields()");
|
|
}
|
|
|
|
var sql = 'SELECT fieldID FROM itemTypeFields '
|
|
+ 'WHERE itemTypeID=' + itemTypeID + ' ORDER BY orderIndex';
|
|
var fields = Zotero.DB.columnQuery(sql);
|
|
|
|
_itemTypeFields[itemTypeID] = fields ? fields : [];
|
|
return _itemTypeFields[itemTypeID];
|
|
}
|
|
|
|
|
|
function isBaseField(field) {
|
|
if (!_fieldsLoaded){
|
|
_loadFields();
|
|
}
|
|
|
|
_fieldCheck(field, arguments.callee.name);
|
|
|
|
return _fields[field]['isBaseField'];
|
|
}
|
|
|
|
|
|
function isFieldOfBase(field, baseField) {
|
|
var fieldID = _fieldCheck(field, 'isFieldOfBase');
|
|
|
|
var baseFieldID = this.getID(baseField);
|
|
if (!baseFieldID) {
|
|
throw ("Invalid field '" + baseField + '" for base field in ItemFields.getFieldIDFromTypeAndBase()');
|
|
}
|
|
|
|
if (fieldID == baseFieldID) {
|
|
return true;
|
|
}
|
|
|
|
var typeFields = this.getTypeFieldsFromBase(baseFieldID);
|
|
return typeFields.indexOf(fieldID) != -1;
|
|
}
|
|
|
|
|
|
function getBaseMappedFields() {
|
|
return Zotero.DB.columnQuery("SELECT DISTINCT fieldID FROM baseFieldMappings");
|
|
}
|
|
|
|
|
|
/*
|
|
* Returns the fieldID of a type-specific field for a given base field
|
|
* or false if none
|
|
*
|
|
* Examples:
|
|
*
|
|
* 'audioRecording' and 'publisher' returns label's fieldID
|
|
* 'book' and 'publisher' returns publisher's fieldID
|
|
* 'audioRecording' and 'number' returns false
|
|
*
|
|
* Accepts names or ids
|
|
*/
|
|
function getFieldIDFromTypeAndBase(itemType, baseField) {
|
|
if (!_fieldsLoaded){
|
|
_loadFields();
|
|
}
|
|
|
|
var itemTypeID = Zotero.ItemTypes.getID(itemType);
|
|
if (!itemTypeID) {
|
|
throw ("Invalid item type '" + itemType + "' in ItemFields.getFieldIDFromTypeAndBase()");
|
|
}
|
|
|
|
var baseFieldID = this.getID(baseField);
|
|
if (!baseFieldID) {
|
|
throw ("Invalid field '" + baseField + '" for base field in ItemFields.getFieldIDFromTypeAndBase()');
|
|
}
|
|
|
|
return _baseTypeFields[itemTypeID][baseFieldID];
|
|
}
|
|
|
|
|
|
/*
|
|
* Returns the fieldID of the base field for a given type-specific field
|
|
* or false if none
|
|
*
|
|
* Examples:
|
|
*
|
|
* 'audioRecording' and 'label' returns publisher's fieldID
|
|
* 'book' and 'publisher' returns publisher's fieldID
|
|
* 'audioRecording' and 'runningTime' returns false
|
|
*
|
|
* Accepts names or ids
|
|
*/
|
|
function getBaseIDFromTypeAndField(itemType, typeField) {
|
|
var itemTypeID = Zotero.ItemTypes.getID(itemType);
|
|
var typeFieldID = this.getID(typeField);
|
|
|
|
if (!itemTypeID) {
|
|
throw ("Invalid item type '" + itemType + "' in ItemFields.getBaseIDFromTypeAndField()");
|
|
}
|
|
|
|
_fieldCheck(typeField, 'getBaseIDFromTypeAndField');
|
|
|
|
if (!this.isValidForType(typeFieldID, itemTypeID)) {
|
|
throw ("'" + typeField + "' is not a valid field for '" + itemType + "' in ItemFields.getBaseIDFromTypeAndField()");
|
|
}
|
|
|
|
// If typeField is already a base field, just return that
|
|
if (this.isBaseField(typeFieldID)) {
|
|
return typeFieldID;
|
|
}
|
|
|
|
return Zotero.DB.valueQuery("SELECT baseFieldID FROM baseFieldMappings "
|
|
+ "WHERE itemTypeID=? AND fieldID=?", [itemTypeID, typeFieldID]);
|
|
}
|
|
|
|
|
|
/*
|
|
* Returns an array of fieldIDs associated with a given base field
|
|
*
|
|
* e.g. 'publisher' returns fieldIDs for [university, studio, label, network]
|
|
*/
|
|
function getTypeFieldsFromBase(baseField, asNames) {
|
|
var baseFieldID = this.getID(baseField);
|
|
if (!baseFieldID) {
|
|
throw ("Invalid base field '" + baseField + '" in ItemFields.getTypeFieldsFromBase()');
|
|
}
|
|
|
|
if (asNames) {
|
|
return _typeFieldNamesByBase[baseFieldID] ?
|
|
_typeFieldNamesByBase[baseFieldID] : false;
|
|
}
|
|
|
|
return _typeFieldIDsByBase[baseFieldID] ?
|
|
_typeFieldIDsByBase[baseFieldID] : false;
|
|
}
|
|
|
|
|
|
/**
|
|
* Check whether a field is valid, throwing an exception if not
|
|
* (since it should never actually happen)
|
|
**/
|
|
function _fieldCheck(field, func) {
|
|
var fieldID = self.getID(field);
|
|
if (!fieldID) {
|
|
throw ("Invalid field '" + field + (func ? "' in ItemFields." + func + "()" : "'"));
|
|
}
|
|
return fieldID;
|
|
}
|
|
|
|
|
|
/*
|
|
* Returns hash array of itemTypeIDs for which a given field is valid
|
|
*/
|
|
function _getFieldItemTypes(){
|
|
var sql = 'SELECT fieldID, itemTypeID FROM itemTypeFields';
|
|
|
|
var results = Zotero.DB.query(sql);
|
|
|
|
if (!results){
|
|
throw ('No fields in itemTypeFields!');
|
|
}
|
|
var fields = new Array();
|
|
for (var i=0; i<results.length; i++){
|
|
if (!fields[results[i]['fieldID']]){
|
|
fields[results[i]['fieldID']] = new Array();
|
|
}
|
|
fields[results[i]['fieldID']][results[i]['itemTypeID']] = true;
|
|
}
|
|
return fields;
|
|
}
|
|
|
|
|
|
/*
|
|
* Build a lookup table for base field mappings
|
|
*/
|
|
function _loadBaseTypeFields() {
|
|
// Grab all fields, base field or not
|
|
var sql = "SELECT IT.itemTypeID, F.fieldID AS baseFieldID, BFM.fieldID "
|
|
+ "FROM itemTypes IT LEFT JOIN fields F "
|
|
+ "LEFT JOIN baseFieldMappings BFM"
|
|
+ " ON (IT.itemTypeID=BFM.itemTypeID AND F.fieldID=BFM.baseFieldID)";
|
|
var rows = Zotero.DB.query(sql);
|
|
|
|
var sql = "SELECT DISTINCT baseFieldID FROM baseFieldMappings";
|
|
var baseFields = Zotero.DB.columnQuery(sql);
|
|
|
|
var fields = [];
|
|
for each(var row in rows) {
|
|
if (!fields[row.itemTypeID]) {
|
|
fields[row.itemTypeID] = [];
|
|
}
|
|
if (row.fieldID) {
|
|
fields[row.itemTypeID][row.baseFieldID] = row.fieldID;
|
|
}
|
|
// If a base field and already valid for the type, just use that
|
|
else if (isBaseField(row.baseFieldID) &&
|
|
isValidForType(row.baseFieldID, row.itemTypeID)) {
|
|
fields[row.itemTypeID][row.baseFieldID] = row.baseFieldID;
|
|
}
|
|
// Set false for other fields so that we don't need to test for
|
|
// existence
|
|
else {
|
|
fields[row.itemTypeID][row.baseFieldID] = false;
|
|
}
|
|
}
|
|
|
|
_baseTypeFields = fields;
|
|
|
|
|
|
var sql = "SELECT baseFieldID, fieldID, fieldName "
|
|
+ "FROM baseFieldMappings JOIN fields USING (fieldID)";
|
|
var rows = Zotero.DB.query(sql);
|
|
for each(var row in rows) {
|
|
if (!_typeFieldIDsByBase[row['baseFieldID']]) {
|
|
_typeFieldIDsByBase[row['baseFieldID']] = [];
|
|
_typeFieldNamesByBase[row['baseFieldID']] = [];
|
|
}
|
|
_typeFieldIDsByBase[row['baseFieldID']].push(row['fieldID']);
|
|
_typeFieldNamesByBase[row['baseFieldID']].push(row['fieldName']);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* Load all fields into an internal hash array
|
|
*/
|
|
function _loadFields(){
|
|
var result = Zotero.DB.query('SELECT * FROM fieldFormats');
|
|
|
|
for (var i=0; i<result.length; i++){
|
|
_fieldFormats[result[i]['fieldFormatID']] = {
|
|
regex: result[i]['regex'],
|
|
isInteger: result[i]['isInteger']
|
|
};
|
|
}
|
|
|
|
var fields = Zotero.DB.query('SELECT * FROM fields');
|
|
|
|
var fieldItemTypes = _getFieldItemTypes();
|
|
|
|
var sql = "SELECT DISTINCT baseFieldID FROM baseFieldMappings";
|
|
var baseFields = Zotero.DB.columnQuery(sql);
|
|
|
|
for each(var field in fields){
|
|
_fields[field['fieldID']] = {
|
|
id: field['fieldID'],
|
|
name: field['fieldName'],
|
|
isBaseField: (baseFields.indexOf(field['fieldID']) != -1),
|
|
formatID: field['fieldFormatID'],
|
|
itemTypes: fieldItemTypes[field['fieldID']]
|
|
};
|
|
// Store by name as well as id
|
|
_fields[field['fieldName']] = _fields[field['fieldID']];
|
|
}
|
|
|
|
_fieldsLoaded = true;
|
|
|
|
_loadBaseTypeFields();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
* Zotero.getCollections(parent)
|
|
*
|
|
* Returns an array of all collections are children of a collection
|
|
* as Zotero.Collection instances
|
|
*
|
|
* Takes parent collectionID as optional parameter;
|
|
* by default, returns root collections
|
|
*/
|
|
Zotero.getCollections = function(parent, recursive){
|
|
var toReturn = new Array();
|
|
|
|
if (!parent){
|
|
parent = null;
|
|
}
|
|
|
|
var sql = 'SELECT collectionID FROM collections C WHERE parentCollectionID';
|
|
sql += parent ? '=' + parent : ' IS NULL';
|
|
|
|
sql += ' ORDER BY collectionName COLLATE NOCASE';
|
|
|
|
var children = Zotero.DB.columnQuery(sql);
|
|
|
|
if (!children){
|
|
Zotero.debug('No child collections of collection ' + parent, 5);
|
|
return toReturn;
|
|
}
|
|
|
|
for (var i=0, len=children.length; i<len; i++){
|
|
var obj = Zotero.Collections.get(children[i]);
|
|
if (!obj){
|
|
throw ('Collection ' + children[i] + ' not found');
|
|
}
|
|
|
|
toReturn.push(obj);
|
|
|
|
// If recursive, get descendents
|
|
if (recursive){
|
|
var desc = obj.getDescendents(false, 'collection');
|
|
for (var j in desc){
|
|
var obj2 = Zotero.Collections.get(desc[j]['id']);
|
|
if (!obj2){
|
|
throw ('Collection ' + desc[j] + ' not found');
|
|
}
|
|
|
|
// TODO: This is a quick hack so that we can indent subcollections
|
|
// in the search dialog -- ideally collections would have a
|
|
// getLevel() method, but there's no particularly quick way
|
|
// of calculating that without either storing it in the DB or
|
|
// changing the schema to Modified Preorder Tree Traversal,
|
|
// and I don't know if we'll actually need it anywhere else.
|
|
obj2.level = desc[j].level;
|
|
|
|
toReturn.push(obj2);
|
|
}
|
|
}
|
|
}
|
|
|
|
return toReturn;
|
|
}
|
|
|
|
|
|
/*
|
|
* Zotero.getItems(parent)
|
|
*
|
|
* Returns an array of all items that are children of a collection--or all
|
|
* items if no parent provided--as Zotero.Item instances
|
|
*/
|
|
Zotero.getItems = function(parent){
|
|
var toReturn = new Array();
|
|
|
|
if (!parent){
|
|
// Not child items
|
|
var sql = "SELECT A.itemID FROM items A LEFT JOIN itemNotes B USING (itemID) "
|
|
+ "LEFT JOIN itemAttachments C ON (C.itemID=A.itemID) WHERE B.sourceItemID IS NULL"
|
|
+ " AND C.sourceItemID IS NULL";
|
|
}
|
|
else {
|
|
var sql = 'SELECT itemID FROM collectionItems '
|
|
+ 'WHERE collectionID=' + parent;
|
|
}
|
|
|
|
var children = Zotero.DB.columnQuery(sql);
|
|
|
|
if (!children){
|
|
if (!parent){
|
|
Zotero.debug('No items in library', 5);
|
|
}
|
|
else {
|
|
Zotero.debug('No child items of collection ' + parent, 5);
|
|
}
|
|
return toReturn;
|
|
}
|
|
|
|
return Zotero.Items.get(children);
|
|
}
|
|
|
|
|
|
Zotero.getAttachments = function(){
|
|
var toReturn = [];
|
|
|
|
var sql = "SELECT A.itemID FROM items A JOIN itemAttachments B ON "
|
|
+ "(B.itemID=A.itemID) WHERE B.sourceItemID IS NULL";
|
|
var items = Zotero.DB.query(itemAttachments);
|
|
|
|
return Zotero.Items.get(items);
|
|
}
|