commit 91459f95f747aa6cfa06952d530d2d53751dab7f
parent 1db1de2257c66a528ea252f72d0644e948c381eb
Author: Dan Stillman <dstillman@zotero.org>
Date: Thu, 14 May 2009 18:23:40 +0000
2.0b3 megacommit
- Support for group libraries
- General support for multiple libraries of different types
- Streamlined sync support
- Using solely libraryID and key rather than itemID, and removed all itemID-changing code
- Combined two requests for increased performance and decreased server load
- Added warning on user account change
- Provide explicit error message on SSL failure
- Removed snapshot and link toolbar buttons and changed browser context menu options and drags to create parent items + snapshots
- Closes #786, Add numPages field
- Fixes #1063, Duplicate item with tags broken in Sync Preview
- Added better purging of deleted tags
- Added local user key before first sync
- Add clientDateModified to all objects for more flexibility in syncing
- Added new triples-based Relation object type, currently used to store links between items copied between local and group libraries
- Updated zotero.org translator for groups
- Additional trigger-based consistency checks
- Fixed broken URL drag in Firefox 3.5
- Disabled zeroconf menu option (no longer functional)
Developer-specific changes:
- Overhauled data layer
- Data object constructors no longer take arguments (return to 1.0-like API)
- Existing objects can be retrieved by setting id or library/key properties
- id/library/key must be set for new objects before other fields
- New methods:
- ZoteroPane.getSelectedLibraryID()
- ZoteroPane.getSelectedGroup(asID)
- ZoteroPane.addItemFromDocument(doc, itemType, saveSnapshot)
- ZoteroPane.addItemFromURL(url, itemType)
- ZoteroPane.canEdit()
- Zotero.CollectionTreeView.selectLibrary(libraryID)
- New Zotero.URI methods
- Changed methods
- Many data object methods now take a libraryID
- ZoteroPane.addAttachmentFromPage(link, itemID)
- Removed saveItem and saveAttachments parameters from Zotero.Translate constructor
- translate() now takes a libraryID, null for local library, or false to not save items (previously on constructor)
- saveAttachments is now a translate() parameter
- Zotero.flattenArguments() better handles passed objects
- Zotero.File.getFileHash() (not currently used)
Diffstat:
55 files changed, 5349 insertions(+), 1795 deletions(-)
diff --git a/chrome/content/zotero/advancedSearch.js b/chrome/content/zotero/advancedSearch.js
@@ -29,7 +29,14 @@ var ZoteroAdvancedSearch = new function() {
var itemGroup = {
isSearchMode: function() { return true; },
getChildItems: function () {
- var ids = _searchBox.search.search();
+ var search = _searchBox.search.clone();
+ // FIXME: Hack to exclude group libraries for now
+ var groups = Zotero.Groups.getAll();
+ for each(var group in groups) {
+ search.addCondition('libraryID', 'isNot', group.libraryID);
+ }
+ //var search = _searchBox.search;
+ var ids = search.search();
return Zotero.Items.get(ids);
},
isLibrary: function () { return false; },
diff --git a/chrome/content/zotero/bindings/itembox.xml b/chrome/content/zotero/bindings/itembox.xml
@@ -407,7 +407,7 @@
this._tabIndexMaxInfoFields = Math.max(this._tabIndexMaxInfoFields, tabindex);
if (fieldIsClickable &&
- !this.item.isPrimaryField(fieldName) &&
+ !Zotero.Items.isPrimaryField(fieldName) &&
Zotero.ItemFields.isFieldOfBase(Zotero.ItemFields.getID(fieldName), 'date')) {
this.addDateRow(fieldNames[i], this.item.getField(fieldName, true), tabindex);
continue;
@@ -1688,7 +1688,7 @@
<body>
<![CDATA[
return !this.clickByRow &&
- ((this.clickable && !this.item.isPrimaryField(fieldName))
+ ((this.clickable && !Zotero.Items.isPrimaryField(fieldName))
|| this._clickableFields.indexOf(fieldName) != -1);
]]>
</body>
@@ -1802,6 +1802,7 @@
<parameter name="changeGlobally"/>
<body>
<![CDATA[
+ var libraryID = this.item.libraryID;
var firstName = fields.firstName;
var lastName = fields.lastName;
//var shortName = fields.shortName;
@@ -1823,16 +1824,17 @@
Zotero.DB.beginTransaction();
var newCreator = new Zotero.Creator;
+ newCreator.libraryID = libraryID;
newCreator.setFields(fields);
var newLinkedCreators = [];
var creatorDataID = Zotero.Creators.getDataID(fields);
if (creatorDataID) {
- newLinkedCreators = Zotero.Creators.getCreatorsWithData(creatorDataID);
+ newLinkedCreators = Zotero.Creators.getCreatorsWithData(creatorDataID, libraryID);
}
if (oldCreator) {
- if (oldCreator.ref.equals(newCreator)) {
+ if (oldCreator.ref.equals(newCreator) || (oldCreator.ref.libraryID != newCreator.libraryID)) {
if (oldCreator.creatorTypeID == creatorTypeID) {
Zotero.debug("Creator " + oldCreator.ref.id + " hasn't changed");
}
@@ -1913,7 +1915,15 @@
this.item.setCreator(index, creator, creatorTypeID);
if (this.saveOnEdit) {
- this.item.save();
+ try {
+ this.item.save();
+ }
+ catch (e) {
+ // DEBUG: Errors aren't being logged in Fx3.1b4pre without this
+ Zotero.debug(e);
+ Components.utils.reportError(e);
+ throw (e);
+ }
}
Zotero.DB.commitTransaction();
diff --git a/chrome/content/zotero/bindings/merge.xml b/chrome/content/zotero/bindings/merge.xml
@@ -193,7 +193,7 @@
this._leftpane.objectbox.clickableFields = diffFields;
this._rightpane.objectbox.clickableFields = diffFields;
- var mergeItem = new Zotero.Item(false, this._leftpane.ref.itemTypeID);
+ var mergeItem = new Zotero.Item(this._leftpane.ref.itemTypeID);
this._mergepane.ref = mergeItem;
this._mergepane.objectbox.visibleFields = fields;
}
@@ -428,7 +428,7 @@
value = row.lastChild.getAttribute('itemTypeID');
if (!mergepane.ref) {
- mergepane.ref = new Zotero.Item(false, value);
+ mergepane.ref = new Zotero.Item(value);
}
else {
mergepane.objectbox.changeTypeTo(value, true);
diff --git a/chrome/content/zotero/bindings/noteeditor.xml b/chrome/content/zotero/bindings/noteeditor.xml
@@ -222,7 +222,10 @@
}
// Create new note
- var item = new Zotero.Item(false, 'note');
+ var item = new Zotero.Item('note');
+ if (this.parent) {
+ item.libraryID = this.parent.libraryID;
+ }
item.setNote(noteField.value);
if (this.parent) {
item.setSource(this.parent.id);
@@ -230,7 +233,7 @@
if (this.saveOnEdit) {
var id = item.save();
- if (this.parent && this.collection) {
+ if (!this.parent && this.collection) {
this.collection.addItem(id);
}
}
diff --git a/chrome/content/zotero/bindings/relatedbox.xml b/chrome/content/zotero/bindings/relatedbox.xml
@@ -137,10 +137,18 @@
window.openDialog('chrome://zotero/content/selectItemsDialog.xul', '',
'chrome,dialog=no,modal,centerscreen,resizable=yes', io);
- if(io.dataOut && this.item)
- {
- for(var i = 0; i < io.dataOut.length; i++)
- {
+ if(io.dataOut) {
+ if (io.dataOut.length) {
+ var relItem = Zotero.Items.get(io.dataOut[0]);
+ if (relItem.libraryID != this.item.libraryID) {
+ // FIXME
+ var prompt = Components.classes["@mozilla.org/network/default-prompt;1"]
+ .getService(Components.interfaces.nsIPrompt);
+ prompt.alert("", "You cannot relate items in different libraries in this Zotero release.");
+ return;
+ }
+ }
+ for(var i = 0; i < io.dataOut.length; i++) {
this.item.addRelatedItem(io.dataOut[i]);
}
this.item.save();
diff --git a/chrome/content/zotero/bindings/tagselector.xml b/chrome/content/zotero/bindings/tagselector.xml
@@ -39,6 +39,19 @@
<field name="_dirty">null</field>
<field name="_empty">null</field>
<field name="selection"/>
+
+ <field name="_libraryID"/>
+ <property name="libraryID" onget="return this._libraryID">
+ <setter>
+ <![CDATA[
+ if (this._libraryID != val) {
+ this._dirty = true;
+ }
+ this._libraryID = val;
+ ]]>
+ </setter>
+ </property>
+
<property name="showAutomatic" onget="return this.getAttribute('showAutomatic') != 'false'"/>
<property name="_types">
<getter>
@@ -170,7 +183,7 @@
var tagsToggleBox = this.id('tags-toggle');
if (fetch || this._dirty) {
- this._tags = Zotero.Tags.getAll(this._types);
+ this._tags = Zotero.Tags.getAll(this._types, this.libraryID);
// Remove children
while (tagsToggleBox.hasChildNodes()){
@@ -414,7 +427,7 @@
<![CDATA[
// If a selected tag no longer exists, deselect it
if (event == 'delete') {
- this._tags = Zotero.Tags.getAll(this._types);
+ this._tags = Zotero.Tags.getAll(this._types, this.libraryID);
for (var tag in this.selection) {
for each(var tag2 in this._tags) {
@@ -658,6 +671,17 @@
function onDragOver(event, flavour, session) {
+ /*
+ // TODO: get drop data
+ var ids = dropData.data.split(',');
+ var items = Zotero.Items.get(ids);
+ for (var i=0; i<items.length; i++) {
+ if (!Zotero.Items.isEditable(items[i])) {
+ return false;
+ }
+ }
+ */
+
event.target.setAttribute('draggedOver', true);
return true;
}
@@ -716,7 +740,9 @@
<xul:hbox>
<xul:hbox pack="start">
- <xul:checkbox id="display-all-tags" label="&zotero.tagSelector.displayAll;"
+ <!-- TODO: localize or change -->
+ <!-- <xul:checkbox id="display-all-tags" label="&zotero.tagSelector.displayAll;"-->
+ <xul:checkbox id="display-all-tags" label="Display all tags in this library"
oncommand="var ts = document.getBindingParent(this); ts.filterToScope = !this.checked; event.stopPropagation();">
</xul:checkbox>
</xul:hbox>
diff --git a/chrome/content/zotero/browser.js b/chrome/content/zotero/browser.js
@@ -112,11 +112,14 @@ var Zotero_Browser = new function() {
function(e) { Zotero_Browser.chromeUnload(e) }, false);
}
- /*
- * Scrapes a page (called when the capture icon is clicked); takes a collection
- * ID as the argument
+ /**
+ * Scrapes a page (called when the capture icon is clicked)
+ *
+ * @param {Integer} libraryID
+ * @param {Integer} collectionID
+ * @return void
*/
- function scrapeThisPage(saveLocation) {
+ function scrapeThisPage(libraryID, collectionID) {
if (!Zotero.stateCheck()) {
Zotero_Browser.progress.changeHeadline(Zotero.getString("ingester.scrapeError"));
var desc = Zotero.getString("ingester.scrapeError.transactionInProgress.previousError")
@@ -126,7 +129,7 @@ var Zotero_Browser = new function() {
Zotero_Browser.progress.startCloseTimer(8000);
return;
}
- _getTabObject(this.tabbrowser.selectedBrowser).translate(saveLocation);
+ _getTabObject(this.tabbrowser.selectedBrowser).translate(libraryID, collectionID);
}
/*
@@ -192,6 +195,8 @@ var Zotero_Browser = new function() {
/*
* called to show the collection selection popup
+ *
+ * not currently used
*/
function showPopup(collectionID, parentElement) {
if(_scrapePopupShowing && parentElement.hasChildNodes()) {
@@ -676,19 +681,18 @@ Zotero_Browser.Tab.prototype._attemptLocalFileImport = function(doc) {
}
/*
- * translate a page, saving in saveLocation
+ * translate a page
+ *
+ * @param {Integer} libraryID
+ * @param {Integer} collectionID
*/
-Zotero_Browser.Tab.prototype.translate = function(saveLocation) {
+Zotero_Browser.Tab.prototype.translate = function(libraryID, collectionID) {
if(this.page.translators && this.page.translators.length) {
Zotero_Browser.progress.show();
Zotero_Browser.isScraping = true;
- if(saveLocation) {
- saveLocation = Zotero.Collections.get(saveLocation);
- } else { // save to currently selected collection, if a collection is selected
- try {
- saveLocation = ZoteroPane.getSelectedCollection();
- } catch(e) {}
+ if(collectionID) {
+ collection = Zotero.Collections.get(collectionID);
}
var me = this;
@@ -701,9 +705,9 @@ Zotero_Browser.Tab.prototype.translate = function(saveLocation) {
this.page.hasBeenTranslated = true;
}
this.page.translate.clearHandlers("itemDone");
- this.page.translate.setHandler("itemDone", function(obj, item) { Zotero_Browser.itemDone(obj, item, saveLocation) });
+ this.page.translate.setHandler("itemDone", function(obj, item) { Zotero_Browser.itemDone(obj, item, collection) });
- this.page.translate.translate();
+ this.page.translate.translate(libraryID);
}
}
diff --git a/chrome/content/zotero/fileInterface.js b/chrome/content/zotero/fileInterface.js
@@ -330,7 +330,8 @@ var Zotero_File_Interface = new function() {
} else {
var searchRef = ZoteroPane.getSelectedSavedSearch();
if(searchRef) {
- var search = new Zotero.Search(searchRef.id);
+ var search = new Zotero.Search();
+ search.id = searchRef.id;
name = search.name;
}
}
diff --git a/chrome/content/zotero/lookup.js b/chrome/content/zotero/lookup.js
@@ -18,7 +18,7 @@ const Zotero_Lookup = new function () {
}
}
- var translate = new Zotero.Translate("search", true, false);
+ var translate = new Zotero.Translate("search");
translate.setSearch(item);
// be lenient about translators
@@ -38,15 +38,17 @@ const Zotero_Lookup = new function () {
}
});
- var saveLocation = false;
+ var libraryID = null;
+ var collection = false;
try {
- saveLocation = window.opener.ZoteroPane.getSelectedCollection();
+ libraryID = window.opener.ZoteroPane.getSelectedLibraryID();
+ collection = window.opener.ZoteroPane.getSelectedCollection();
} catch(e) {}
translate.setHandler("itemDone", function(obj, item) {
- if(saveLocation) saveLocation.addItem(item.getID());
+ if(collection) collection.addItem(item.id);
});
- translate.translate();
+ translate.translate(libraryID);
return false;
}
}
diff --git a/chrome/content/zotero/overlay.js b/chrome/content/zotero/overlay.js
@@ -66,7 +66,6 @@ var ZoteroPane = new function()
this.getSortedItems = getSortedItems;
this.getSortField = getSortField;
this.getSortDirection = getSortDirection;
- this.buildCollectionContextMenu = buildCollectionContextMenu;
this.buildItemContextMenu = buildItemContextMenu;
this.onDoubleClick = onDoubleClick;
this.loadURI = loadURI;
@@ -74,11 +73,8 @@ var ZoteroPane = new function()
this.clearItemsPaneMessage = clearItemsPaneMessage;
this.contextPopupShowing = contextPopupShowing;
this.openNoteWindow = openNoteWindow;
- this.newNote = newNote;
this.addTextToNote = addTextToNote;
- this.addItemFromPage = addItemFromPage;
this.addAttachmentFromDialog = addAttachmentFromDialog;
- this.addAttachmentFromPage = addAttachmentFromPage;
this.viewAttachment = viewAttachment;
this.viewSelectedAttachment = viewSelectedAttachment;
this.showAttachmentNotFoundDialog = showAttachmentNotFoundDialog;
@@ -92,6 +88,9 @@ var ZoteroPane = new function()
var self = this;
var titlebarcolorState, toolbarCollapseState, titleState;
+ // Also needs to be changed in collectionTreeView.js
+ var _lastViewedFolderRE = /^(?:(C|S|G)([0-9]+)|L)$/;
+
/*
* Called when the window is open
*/
@@ -136,7 +135,7 @@ var ZoteroPane = new function()
Zotero.setFontSize(document.getElementById('zotero-pane'))
if (Zotero.isMac) {
- document.getElementById('zotero-tb-actions-zeroconf-update').setAttribute('hidden', false);
+ //document.getElementById('zotero-tb-actions-zeroconf-update').setAttribute('hidden', false);
document.getElementById('zotero-pane').setAttribute('platform', 'mac');
} else if(Zotero.isWin) {
document.getElementById('zotero-pane').setAttribute('platform', 'win');
@@ -180,11 +179,6 @@ var ZoteroPane = new function()
Zotero.Keys.windowInit(document);
- // If the database was initialized and Zotero hasn't been run before
- // in this profile, display the Quick Start Guide -- this way the guide
- // won't be displayed when they sync their DB to another profile or if
- // they the DB is initialized erroneously (e.g. while switching data
- // directory locations)
if (Zotero.restoreFromServer) {
Zotero.restoreFromServer = false;
@@ -213,6 +207,11 @@ var ZoteroPane = new function()
}
}, 1000);
}
+ // If the database was initialized and Zotero hasn't been run before
+ // in this profile, display the Quick Start Guide -- this way the guide
+ // won't be displayed when they sync their DB to another profile or if
+ // they the DB is initialized erroneously (e.g. while switching data
+ // directory locations)
else if (Zotero.Schema.dbInitialized && Zotero.Prefs.get('firstRun')) {
setTimeout(function () {
gBrowser.selectedTab = gBrowser.addTab(ZOTERO_CONFIG.FIRST_RUN_URL);
@@ -629,29 +628,42 @@ var ZoteroPane = new function()
return false;
}
- var item = new Zotero.Item(false, typeID);
+ if (!this.canEdit()) {
+ this.displayCannotEditLibraryMessage();
+ return;
+ }
- for (var i in data)
- {
+ var item = new Zotero.Item(typeID);
+ item.libraryID = this.getSelectedLibraryID();
+ for (var i in data) {
item.setField(i, data[i]);
}
-
- item.save();
+ var itemID = item.save();
if (this.itemsView && this.itemsView._itemGroup.isCollection()) {
- this.itemsView._itemGroup.ref.addItem(item.id);
+ this.itemsView._itemGroup.ref.addItem(itemID);
}
//set to Info tab
document.getElementById('zotero-view-item').selectedIndex = 0;
- this.selectItem(item.id);
+ this.selectItem(itemID);
- return item;
+ return Zotero.Items.get(itemID);
}
function newCollection(parent)
{
+ if (!Zotero.stateCheck()) {
+ this.displayErrorMessage(true);
+ return false;
+ }
+
+ if (!this.canEdit()) {
+ this.displayCannotEditLibraryMessage();
+ return;
+ }
+
var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
.getService(Components.interfaces.nsIPromptService);
@@ -674,13 +686,29 @@ var ZoteroPane = new function()
}
var collection = new Zotero.Collection;
+ collection.libraryID = this.getSelectedLibraryID();
collection.name = newName.value;
collection.parent = parent;
collection.save();
}
+
+ this.newGroup = function () {
+ if (this.isFullScreen()) {
+ this.toggleDisplay();
+ }
+
+ window.loadURI(Zotero.Groups.addGroupURL);
+ }
+
+
function newSearch()
{
+ if (!Zotero.stateCheck()) {
+ this.displayErrorMessage(true);
+ return false;
+ }
+
var s = new Zotero.Search();
s.addCondition('title', 'contains', '');
@@ -692,6 +720,21 @@ var ZoteroPane = new function()
}
+ this.openLookupWindow = function () {
+ if (!Zotero.stateCheck()) {
+ this.displayErrorMessage(true);
+ return false;
+ }
+
+ if (!this.canEdit()) {
+ this.displayCannotEditLibraryMessage();
+ return;
+ }
+
+ window.openDialog('chrome://zotero/content/lookup.xul', 'zotero-lookup', 'chrome,modal');
+ }
+
+
function openAdvancedSearchWindow() {
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator);
@@ -825,13 +868,13 @@ var ZoteroPane = new function()
* Passed to the items tree to trigger on changes
*/
function _setTagScope() {
- var itemgroup = self.collectionsView.
- _getItemAtRow(self.collectionsView.selection.currentIndex);
+ var itemGroup = self.collectionsView._getItemAtRow(self.collectionsView.selection.currentIndex);
var tagSelector = document.getElementById('zotero-tag-selector');
if (!tagSelector.getAttribute('collapsed') ||
tagSelector.getAttribute('collapsed') == 'false') {
Zotero.debug('Updating tag selector with current tags');
- tagSelector.scope = itemgroup.getChildTags();
+ tagSelector.libraryID = itemGroup.ref.libraryID;
+ tagSelector.scope = itemGroup.getChildTags();
}
}
@@ -841,45 +884,59 @@ var ZoteroPane = new function()
if (this.itemsView)
{
this.itemsView.unregister();
- document.getElementById('zotero-items-tree').removeEventListener(
- 'keypress', this.itemsView.wrappedJSObject.listener, false
- );
+ if (this.itemsView.wrappedJSObject.listener) {
+ document.getElementById('zotero-items-tree').removeEventListener(
+ 'keypress', this.itemsView.wrappedJSObject.listener, false
+ );
+ }
this.itemsView.wrappedJSObject.listener = null;
document.getElementById('zotero-items-tree').view = this.itemsView = null;
}
document.getElementById('zotero-tb-search').value = "";
- if (this.collectionsView.selection.count == 1 && this.collectionsView.selection.currentIndex != -1) {
- var itemgroup = this.collectionsView._getItemAtRow(this.collectionsView.selection.currentIndex);
- itemgroup.setSearch('');
- itemgroup.setTags(getTagSelection());
- itemgroup.showDuplicates = false;
-
- try {
- Zotero.UnresponsiveScriptIndicator.disable();
- this.itemsView = new Zotero.ItemTreeView(itemgroup);
- this.itemsView.addCallback(_setTagScope);
- document.getElementById('zotero-items-tree').view = this.itemsView;
- this.itemsView.selection.clearSelection();
- }
- finally {
- Zotero.UnresponsiveScriptIndicator.enable();
- }
-
- if (itemgroup.isLibrary()) {
- Zotero.Prefs.set('lastViewedFolder', 'L');
- }
- if (itemgroup.isCollection()) {
- Zotero.Prefs.set('lastViewedFolder', 'C' + itemgroup.ref.id);
- }
- else if (itemgroup.isSearch()) {
- Zotero.Prefs.set('lastViewedFolder', 'S' + itemgroup.ref.id);
- }
+ if (this.collectionsView.selection.count != 1) {
+ document.getElementById('zotero-items-tree').view = this.itemsView = null;
+ return;
}
- else
- {
+
+ // this.collectionsView.selection.currentIndex != -1
+
+ var itemgroup = this.collectionsView._getItemAtRow(this.collectionsView.selection.currentIndex);
+
+ /*
+ if (itemgroup.isSeparator()) {
document.getElementById('zotero-items-tree').view = this.itemsView = null;
+ return;
+ }
+ */
+
+ itemgroup.setSearch('');
+ itemgroup.setTags(getTagSelection());
+ itemgroup.showDuplicates = false;
+
+ try {
+ Zotero.UnresponsiveScriptIndicator.disable();
+ this.itemsView = new Zotero.ItemTreeView(itemgroup);
+ this.itemsView.addCallback(_setTagScope);
+ document.getElementById('zotero-items-tree').view = this.itemsView;
+ this.itemsView.selection.clearSelection();
+ }
+ finally {
+ Zotero.UnresponsiveScriptIndicator.enable();
+ }
+
+ if (itemgroup.isLibrary()) {
+ Zotero.Prefs.set('lastViewedFolder', 'L');
+ }
+ if (itemgroup.isCollection()) {
+ Zotero.Prefs.set('lastViewedFolder', 'C' + itemgroup.ref.id);
+ }
+ else if (itemgroup.isSearch()) {
+ Zotero.Prefs.set('lastViewedFolder', 'S' + itemgroup.ref.id);
+ }
+ else if (itemgroup.isGroup()) {
+ Zotero.Prefs.set('lastViewedFolder', 'G' + itemgroup.ref.id);
}
}
@@ -928,7 +985,7 @@ var ZoteroPane = new function()
tabs.hidden = true;
var noteEditor = document.getElementById('zotero-note-editor');
- noteEditor.mode = this.itemsView.readOnly ? 'view' : 'edit';
+ noteEditor.mode = this.collectionsView.editable ? 'edit' : 'view';
// If loading new or different note, disable undo while we repopulate the text field
// so Undo doesn't end up clearing the field. This also ensures that Undo doesn't
@@ -942,10 +999,7 @@ var ZoteroPane = new function()
noteEditor.enableUndo();
var viewButton = document.getElementById('zotero-view-note-button');
- if (this.itemsView.readOnly) {
- viewButton.hidden = true;
- }
- else {
+ if (this.collectionsView.editable) {
viewButton.hidden = false;
viewButton.setAttribute('noteID', item.ref.id);
if (item.ref.getSource()) {
@@ -955,6 +1009,9 @@ var ZoteroPane = new function()
viewButton.removeAttribute('sourceID');
}
}
+ else {
+ viewButton.hidden = true;
+ }
document.getElementById('zotero-item-pane-content').selectedIndex = 2;
}
@@ -963,7 +1020,7 @@ var ZoteroPane = new function()
tabs.hidden = true;
var attachmentBox = document.getElementById('zotero-attachment-box');
- attachmentBox.mode = this.itemsView.readOnly ? 'view' : 'edit';
+ attachmentBox.mode = this.collectionsView.editable ? 'edit' : 'view';
attachmentBox.item = item.ref;
document.getElementById('zotero-item-pane-content').selectedIndex = 3;
@@ -973,16 +1030,16 @@ var ZoteroPane = new function()
else
{
document.getElementById('zotero-item-pane-content').selectedIndex = 1;
- if (this.itemsView.readOnly) {
- document.getElementById('zotero-view-item').selectedIndex = 0;
- ZoteroItemPane.viewItem(item.ref, 'view');
- tabs.hidden = true;
- }
- else {
+ if (this.collectionsView.editable) {
ZoteroItemPane.viewItem(item.ref);
tabs.selectedIndex = document.getElementById('zotero-view-item').selectedIndex;
tabs.hidden = false;
}
+ else {
+ document.getElementById('zotero-view-item').selectedIndex = 0;
+ ZoteroItemPane.viewItem(item.ref, 'view');
+ tabs.hidden = true;
+ }
}
}
else
@@ -1041,21 +1098,26 @@ var ZoteroPane = new function()
function duplicateSelectedItem() {
- var item = this.getSelectedItems()[0];
- if (item.getTags()) {
- var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
- .getService(Components.interfaces.nsIPromptService);
- ps.alert(null, "Error", "Duplication of tagged items is not available in this Zotero release.");
+ if (!this.canEdit()) {
+ this.displayCannotEditLibraryMessage();
return;
}
- var newItem = this.getSelectedItems()[0].clone();
- var newItemID = newItem.save()
- var newItem = Zotero.Items.get(newItemID);
+ var item = this.getSelectedItems()[0];
+
+ // Create new unsaved clone item in target library
+ var newItem = new Zotero.Item(item.itemTypeID);
+ newItem.libraryID = item.libraryID;
+ // DEBUG: save here because clone() doesn't currently work on unsaved tagged items
+ var id = newItem.save();
+
+ var newItem = Zotero.Items.get(id);
+ item.clone(false, newItem);
+ newItem.save();
if (this.itemsView._itemGroup.isCollection()) {
this.itemsView._itemGroup.ref.addItem(newItem.id);
- this.selectItem(newItemID);
+ this.selectItem(newItem.id);
}
}
@@ -1070,17 +1132,23 @@ var ZoteroPane = new function()
*/
this.deleteSelectedItems = function (force) {
if (this.itemsView && this.itemsView.selection.count > 0) {
+ var itemGroup = this.itemsView._itemGroup;
+
+ if (!itemGroup.isTrash() && !this.canEdit()) {
+ this.displayCannotEditLibraryMessage();
+ return;
+ }
+
if (!force){
- if (this.itemsView._itemGroup.isCollection()) {
+ if (itemGroup.isCollection()) {
var noPrompt = true;
}
// Do nothing in search and share views
- else if (this.itemsView._itemGroup.isSearch() ||
- this.itemsView._itemGroup.isShare()) {
+ else if (itemGroup.isSearch() || itemGroup.isShare()) {
return;
}
// Do nothing in trash view if any non-deleted items are selected
- else if (this.itemsView._itemGroup.isTrash()) {
+ else if (itemGroup.isTrash()) {
var start = {};
var end = {};
for (var i=0, len=this.itemsView.selection.getRangeCount(); i<len; i++) {
@@ -1127,6 +1195,11 @@ var ZoteroPane = new function()
function deleteSelectedCollection()
{
+ if (!this.canEdit()) {
+ this.displayCannotEditLibraryMessage();
+ return;
+ }
+
if (this.collectionsView.selection.count == 1) {
var row =
this.collectionsView._getItemAtRow(this.collectionsView.selection.currentIndex);
@@ -1179,6 +1252,11 @@ var ZoteroPane = new function()
function editSelectedCollection()
{
+ if (!this.canEdit()) {
+ this.displayCannotEditLibraryMessage();
+ return;
+ }
+
if (this.collectionsView.selection.count > 0) {
var row = this.collectionsView._getItemAtRow(this.collectionsView.selection.currentIndex);
@@ -1196,7 +1274,8 @@ var ZoteroPane = new function()
}
}
else {
- var s = new Zotero.Search(row.ref.id);
+ var s = new Zotero.Search();
+ s.id = row.ref.id;
var io = {dataIn: {search: s, name: row.getName()}, dataOut: null};
window.openDialog('chrome://zotero/content/searchDialog.xul','','chrome,modal',io);
if (io.dataOut) {
@@ -1310,24 +1389,55 @@ var ZoteroPane = new function()
function selectItem(itemID, inLibrary, expand)
{
if (!itemID) {
- return;
+ return false;
}
- if (this.itemsView) {
- if (!this.itemsView._itemGroup.isLibrary() && inLibrary) {
- this.collectionsView.selection.select(0);
- }
-
- var selected = this.itemsView.selectItem(itemID, expand);
- if (!selected) {
- this.collectionsView.selection.select(0);
- this.itemsView.selectItem(itemID, expand);
- }
+ var item = Zotero.Items.get(itemID);
+ if (!item) {
+ return false;
+ }
+
+ if (!this.itemsView) {
+ Components.utils.reportError("Items view not set in ZoteroPane.selectItem()");
+ return false;
}
+
+ var currentLibraryID = this.getSelectedLibraryID();
+ // If in a different library
+ if (item.libraryID != currentLibraryID) {
+ this.collectionsView.selectLibrary(item.libraryID);
+ }
+ // Force switch to library view
+ else if (!this.itemsView._itemGroup.isLibrary() && inLibrary) {
+ this.collectionsView.selectLibrary(item.libraryID);
+ }
+
+ var selected = this.itemsView.selectItem(itemID, expand);
+ if (!selected) {
+ this.collectionsView.selectLibrary(item.libraryID);
+ this.itemsView.selectItem(itemID, expand);
+ }
+
+ return true;
+ }
+
+
+ this.getSelectedLibraryID = function () {
+ var group = this.getSelectedGroup();
+ if (group) {
+ return group.libraryID;
+ }
+ var collection = this.getSelectedCollection();
+ if (collection) {
+ return collection.libraryID;
+ }
+ return null;
}
+
function getSelectedCollection(asID) {
- if (this.collectionsView.selection
+ if (this.collectionsView
+ && this.collectionsView.selection
&& this.collectionsView.selection.count > 0
&& this.collectionsView.selection.currentIndex != -1) {
var collection = this.collectionsView._getItemAtRow(this.collectionsView.selection.currentIndex);
@@ -1335,20 +1445,10 @@ var ZoteroPane = new function()
return asID ? collection.ref.id : collection.ref;
}
}
- // If the Zotero pane hasn't yet been opened, use the lastViewedFolder pref
- else {
- var lastViewedFolder = Zotero.Prefs.get('lastViewedFolder');
- var matches = lastViewedFolder.match(/^(?:(C|S)([0-9]+)|L)$/);
- if (matches && matches[1] == 'C') {
- var col = Zotero.Collections.get(matches[2]);
- if (col) {
- return asID ? col.id : col;
- }
- }
- }
return false;
}
+
function getSelectedSavedSearch(asID)
{
if (this.collectionsView.selection.count > 0 && this.collectionsView.selection.currentIndex != -1) {
@@ -1357,20 +1457,10 @@ var ZoteroPane = new function()
return asID ? collection.ref.id : collection.ref;
}
}
- // If the Zotero pane hasn't yet been opened, use the lastViewedFolder pref
- else {
- var lastViewedFolder = Zotero.Prefs.get('lastViewedFolder');
- var matches = lastViewedFolder.match(/^(?:(C|S)([0-9]+)|L)$/);
- if (matches && matches[1] == 'S') {
- var search = Zotero.Search.get(matches[2]);
- if (search) {
- return asID ? search.id : search;
- }
- }
- }
return false;
}
+
/*
* Return an array of Item objects for selected items
*
@@ -1386,6 +1476,19 @@ var ZoteroPane = new function()
}
+ this.getSelectedGroup = function (asID) {
+ if (this.collectionsView.selection
+ && this.collectionsView.selection.count > 0
+ && this.collectionsView.selection.currentIndex != -1) {
+ var itemGroup = this.collectionsView._getItemAtRow(this.collectionsView.selection.currentIndex);
+ if (itemGroup && itemGroup.isGroup()) {
+ return asID ? itemGroup.ref.id : itemGroup.ref;
+ }
+ }
+ return false;
+ }
+
+
/*
* Returns an array of Zotero.Item objects of visible items in current sort order
*
@@ -1418,10 +1521,9 @@ var ZoteroPane = new function()
}
- function buildCollectionContextMenu()
+ this.buildCollectionContextMenu = function buildCollectionContextMenu()
{
var menu = document.getElementById('zotero-collectionmenu');
-
var m = {
newCollection: 0,
newSavedSearch: 1,
@@ -1437,22 +1539,32 @@ var ZoteroPane = new function()
emptyTrash: 11
};
+ var itemGroup = this.collectionsView._getItemAtRow(this.collectionsView.selection.currentIndex);
+
+ var enable = [], disable = [], show = [];
+
// Collection
- if (this.collectionsView.selection.count == 1 &&
- this.collectionsView._getItemAtRow(this.collectionsView.selection.currentIndex).isCollection())
- {
- var show = [m.newSubcollection, m.sep1, m.editSelectedCollection, m.removeCollection,
- m.sep2, m.exportCollection, m.createBibCollection, m.loadReport];
+ if (itemGroup.isCollection()) {
+ show = [
+ m.newSubcollection,
+ m.sep1,
+ m.editSelectedCollection,
+ m.removeCollection,
+ m.sep2,
+ m.exportCollection,
+ m.createBibCollection,
+ m.loadReport
+ ];
+ var s = [m.exportCollection, m.createBibCollection, m.loadReport];
if (this.itemsView.rowCount>0) {
- var enable = [m.exportCollection, m.createBibCollection, m.loadReport];
+ enable = s;
}
else if (!this.collectionsView.isContainerEmpty(this.collectionsView.selection.currentIndex)) {
- var enable = [m.exportCollection];
- var disable = [m.createBibCollection, m.loadReport];
+ enable = [m.exportCollection];
+ disable = [m.createBibCollection, m.loadReport];
}
- else
- {
- var disable = [m.exportCollection, m.createBibCollection, m.loadReport];
+ else {
+ disable = s;
}
// Adjust labels
@@ -1463,17 +1575,22 @@ var ZoteroPane = new function()
menu.childNodes[m.loadReport].setAttribute('label', Zotero.getString('pane.collections.menu.generateReport.collection'));
}
// Saved Search
- else if (this.collectionsView.selection.count == 1 &&
- this.collectionsView._getItemAtRow(this.collectionsView.selection.currentIndex).isSearch()) {
- var show = [m.editSelectedCollection, m.removeCollection, m.sep2, m.exportCollection,
- m.createBibCollection, m.loadReport];
+ else if (itemGroup.isSearch()) {
+ show = [
+ m.editSelectedCollection,
+ m.removeCollection,
+ m.sep2,
+ m.exportCollection,
+ m.createBibCollection,
+ m.loadReport
+ ];
+ var s = [m.exportCollection, m.createBibCollection, m.loadReport];
if (this.itemsView.rowCount>0) {
- var enable = [m.exportCollection, m.createBibCollection, m.loadReport];
+ enable = s;
}
- else
- {
- var disable = [m.exportCollection, m.createBibCollection, m.loadReport];
+ else {
+ disable = s;
}
// Adjust labels
@@ -1484,14 +1601,30 @@ var ZoteroPane = new function()
menu.childNodes[m.loadReport].setAttribute('label', Zotero.getString('pane.collections.menu.generateReport.savedSearch'));
}
// Trash
- else if (this.collectionsView.selection.count == 1 &&
- this.collectionsView._getItemAtRow(this.collectionsView.selection.currentIndex).isTrash()) {
- var show = [m.emptyTrash];
+ else if (itemGroup.isTrash()) {
+ show = [m.emptyTrash];
+ }
+ // Header
+ else if (itemGroup.isHeader()) {
+
+ }
+ // Group
+ else if (itemGroup.isGroup()) {
+ show = [m.newCollection];
}
// Library
else
{
- var show = [m.newCollection, m.newSavedSearch, m.sep1, m.exportFile];
+ show = [m.newCollection, m.newSavedSearch, m.sep1, m.exportFile];
+ }
+
+ // Disable some actions if user doesn't have write access
+ var s = [m.editSelectedCollection, m.removeCollection, m.newCollection, m.newSavedSearch];
+ if (itemGroup.isGroup() && !itemGroup.ref.editable) {
+ disable = disable.concat(s);
+ }
+ else {
+ enable = enable.concat(s);
}
for (var i in disable)
@@ -1541,7 +1674,7 @@ var ZoteroPane = new function()
var enable = [], disable = [], show = [], hide = [], multiple = '';
// TODO: implement menu for remote items
- if (this.itemsView.readOnly) {
+ if (!this.collectionsView.editable) {
for each(var pos in m) {
disable.push(pos);
}
@@ -1725,13 +1858,23 @@ var ZoteroPane = new function()
}
if (tree.id == 'zotero-collections-tree') {
- var s = this.getSelectedSavedSearch();
- if (s) {
+ var itemGroup = this.collectionsView._getItemAtRow(this.collectionsView.selection.currentIndex);
+ if (itemGroup.isSearch()) {
this.editSelectedCollection();
}
+ else if (itemGroup.isGroup()) {
+ var uri = Zotero.URI.getGroupURI(itemGroup.ref, true);
+ window.loadURI(uri);
+ }
+ else if (itemGroup.isHeader()) {
+ if (itemGroup.ref.id == 'group-libraries-header') {
+ var uri = Zotero.URI.getGroupsURL();
+ window.loadURI(uri);
+ }
+ }
}
else if (tree.id == 'zotero-items-tree') {
- if (this.itemsView.readOnly) {
+ if (!this.collectionsView.editable) {
return;
}
@@ -1893,7 +2036,7 @@ var ZoteroPane = new function()
}
}
- var menuitem = document.getElementById("zotero-context-save-link-as-snapshot");
+ var menuitem = document.getElementById("zotero-context-save-link-as-item");
if (menuitem) {
if (window.gContextMenu.onLink) {
menuitem.hidden = false;
@@ -1904,7 +2047,7 @@ var ZoteroPane = new function()
}
}
- var menuitem = document.getElementById("zotero-context-save-image-as-snapshot");
+ var menuitem = document.getElementById("zotero-context-save-image-as-item");
if (menuitem) {
// Not using window.gContextMenu.hasBGImage -- if the user wants it,
// they can use the Firefox option to view and then import from there
@@ -1922,26 +2065,32 @@ var ZoteroPane = new function()
}
- function newNote(popup, parent, text) {
+ this.newNote = function (popup, parent, text) {
if (!Zotero.stateCheck()) {
this.displayErrorMessage(true);
return;
}
+ if (!this.canEdit()) {
+ this.displayCannotEditLibraryMessage();
+ return;
+ }
+
if (!popup) {
if (!text) {
text = '';
}
text = Zotero.Utilities.prototype.trim(text);
- var item = new Zotero.Item(false, 'note');
+ var item = new Zotero.Item('note');
+ item.libraryID = this.getSelectedLibraryID();
item.setNote(text);
if (parent) {
item.setSource(parent);
}
var itemID = item.save();
- if (this.itemsView && this.itemsView._itemGroup.isCollection()) {
+ if (!parent && this.itemsView && this.itemsView._itemGroup.isCollection()) {
this.itemsView._itemGroup.ref.addItem(itemID);
}
@@ -1964,6 +2113,11 @@ var ZoteroPane = new function()
function addTextToNote(text)
{
+ if (!this.canEdit()) {
+ this.displayCannotEditLibraryMessage();
+ return;
+ }
+
try {
// trim
text = text.replace(/^[\xA0\r\n\s]*(.*)[\xA0\r\n\s]*$/m, "$1");
@@ -1996,6 +2150,11 @@ var ZoteroPane = new function()
function openNoteWindow(itemID, col, parentItemID)
{
+ if (!this.canEdit()) {
+ this.displayCannotEditLibraryMessage();
+ return;
+ }
+
var name = null;
if (itemID) {
@@ -2026,6 +2185,20 @@ var ZoteroPane = new function()
function addAttachmentFromDialog(link, id)
{
+ if (!this.canEdit()) {
+ this.displayCannotEditLibraryMessage();
+ return;
+ }
+
+ // FIXME: temporarily disable file attachment options for groups
+ var itemGroup = this.collectionsView._getItemAtRow(this.collectionsView.selection.currentIndex);
+ if (itemGroup.isWithinGroup()) {
+ var pr = Components.classes["@mozilla.org/network/default-prompt;1"]
+ .getService(Components.interfaces.nsIPrompt);
+ pr.alert("", "Files cannot currently be added to group libraries.");
+ return;
+ }
+
var nsIFilePicker = Components.interfaces.nsIFilePicker;
var fp = Components.classes["@mozilla.org/filepicker;1"]
.createInstance(nsIFilePicker);
@@ -2055,80 +2228,120 @@ var ZoteroPane = new function()
}
- function addItemFromPage() {
+ this.addItemFromPage = function (itemType) {
+ return this.addItemFromDocument(window.content.document, itemType);
+ }
+
+
+ /**
+ * @param {Document} doc
+ * @param {String|Integer} [itemType='webpage'] Item type id or name
+ * @param {Boolean} [saveSnapshot] Force saving of a snapshot,
+ * regardless of automaticSnapshots pref
+ */
+ this.addItemFromDocument = function (doc, itemType, saveSnapshot) {
if (!Zotero.stateCheck()) {
this.displayErrorMessage(true);
return false;
}
+ if (!this.canEdit()) {
+ this.displayCannotEditLibraryMessage();
+ return;
+ }
+
var progressWin = new Zotero.ProgressWindow();
progressWin.changeHeadline(Zotero.getString('ingester.scraping'));
var icon = 'chrome://zotero/skin/treeitem-webpage.png';
- progressWin.addLines(window.content.document.title, icon)
+ progressWin.addLines(doc.title, icon)
progressWin.show();
progressWin.startCloseTimer();
var data = {
- title: window.content.document.title,
- url: window.content.document.location.href,
+ title: doc.title,
+ url: doc.location.href,
accessDate: "CURRENT_TIMESTAMP"
}
- var item = this.newItem(Zotero.ItemTypes.getID('webpage'), data);
+ // Save web page item by default
+ if (!itemType) {
+ itemType = 'webpage';
+ }
+ itemType = Zotero.ItemTypes.getID(itemType);
+ var item = this.newItem(itemType, data);
- // Automatically save snapshot if pref set
- if (item.id && Zotero.Prefs.get('automaticSnapshots'))
- {
- var f = function() {
- // We set |noParent|, since child items don't belong to collections
- ZoteroPane.addAttachmentFromPage(false, item.id, true);
+ var filesEditable = false;
+ if (item.libraryID) {
+ var group = Zotero.Groups.getByLibraryID(item.libraryID);
+ filesEditable = group.filesEditable;
+ }
+
+ // Save snapshot if explicitly enabled or automatically pref is set and not explicitly disabled
+ if (saveSnapshot || (saveSnapshot !== false && Zotero.Prefs.get('automaticSnapshots'))) {
+ var link = false;
+
+ if (link) {
+ Zotero.Attachments.linkFromDocument(doc, item.id);
+ }
+ else if (filesEditable) {
+ Zotero.Attachments.importFromDocument(doc, item.id);
}
- // Give progress window time to appear
- setTimeout(f, 300);
}
return item.id;
}
+ this.addItemFromURL = function (url, itemType) {
+ if (url == window.content.document.location.href) {
+ return this.addItemFromPage(itemType);
+ }
+
+ var processor = function (doc) {
+ ZoteroPane.addItemFromDocument(doc, itemType);
+ };
+
+ var done = function () {}
+
+ var exception = function (e) {
+ Zotero.debug(e);
+ }
+
+ Zotero.Utilities.HTTP.processDocuments([url], processor, done, exception);
+ }
+
+
/*
* Create an attachment from the current page
*
- * |link| -- create web link instead of snapshot
* |itemID| -- itemID of parent item
- * |noParent| -- don't add to current collection
+ * |link| -- create web link instead of snapshot
*/
- function addAttachmentFromPage(link, itemID, noParent)
+ this.addAttachmentFromPage = function (link, itemID)
{
if (!Zotero.stateCheck()) {
this.displayErrorMessage(true);
return;
}
- if (!noParent) {
- var progressWin = new Zotero.ProgressWindow();
- progressWin.changeHeadline(Zotero.getString('save.' + (link ? 'link' : 'attachment')));
- var type = link ? 'web-link' : 'snapshot';
- var icon = 'chrome://zotero/skin/treeitem-attachment-' + type + '.png';
- progressWin.addLines(window.content.document.title, icon)
- progressWin.show();
- progressWin.startCloseTimer();
-
- if (this.itemsView && this.itemsView._itemGroup.isCollection()) {
- var parentCollectionID = this.itemsView._itemGroup.ref.id;
- }
+ if (typeof itemID != 'number') {
+ throw ("itemID must be an integer in ZoteroPane.addAttachmentFromPage()");
}
- var f = function() {
- if (link) {
- Zotero.Attachments.linkFromDocument(window.content.document, itemID, parentCollectionID);
- }
- else {
- Zotero.Attachments.importFromDocument(window.content.document, itemID, false, parentCollectionID);
- }
+ var progressWin = new Zotero.ProgressWindow();
+ progressWin.changeHeadline(Zotero.getString('save.' + (link ? 'link' : 'attachment')));
+ var type = link ? 'web-link' : 'snapshot';
+ var icon = 'chrome://zotero/skin/treeitem-attachment-' + type + '.png';
+ progressWin.addLines(window.content.document.title, icon)
+ progressWin.show();
+ progressWin.startCloseTimer();
+
+ if (link) {
+ Zotero.Attachments.linkFromDocument(window.content.document, itemID);
+ }
+ else {
+ Zotero.Attachments.importFromDocument(window.content.document, itemID);
}
- // Give progress window time to appear
- setTimeout(f, 100);
}
@@ -2225,6 +2438,28 @@ var ZoteroPane = new function()
}
+ /**
+ * Test if the user can edit the currently selected library/collection,
+ * and display an error if not
+ *
+ * @return {Boolean} TRUE if user can edit, FALSE if not
+ */
+ this.canEdit = function () {
+ var itemGroup = this.collectionsView._getItemAtRow(this.collectionsView.selection.currentIndex);
+ return itemGroup.isEditable();
+ }
+
+
+ this.displayCannotEditLibraryMessage = function () {
+ var pr = Components.classes["@mozilla.org/network/default-prompt;1"]
+ .getService(Components.interfaces.nsIPrompt);
+ pr.alert(
+ Zotero.getString('general.accessDenied'),
+ "You cannot make changes to the currently selected library."
+ );
+ }
+
+
function showAttachmentNotFoundDialog(itemID, noLocate) {
var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"].
createInstance(Components.interfaces.nsIPromptService);
@@ -2254,6 +2489,11 @@ var ZoteroPane = new function()
function relinkAttachment(itemID) {
+ if (!this.canEdit()) {
+ this.displayCannotEditLibraryMessage();
+ return;
+ }
+
var item = Zotero.Items.get(itemID);
if (!item) {
throw('Item ' + itemID + ' not found in ZoteroPane.relinkAttachment()');
diff --git a/chrome/content/zotero/overlay.xul b/chrome/content/zotero/overlay.xul
@@ -60,12 +60,13 @@
<menuitem id="zotero-context-add-to-new-note" class="menu-iconic"
label="&zotero.contextMenu.addTextToNewNote;" hidden="true"
oncommand="var str = event.currentTarget.ownerDocument.popupNode.ownerDocument.defaultView.getSelection().toString(); var itemID = ZoteroPane.addItemFromPage(); ZoteroPane.newNote(false, itemID, str)"/>
- <menuitem id="zotero-context-save-link-as-snapshot" class="menu-iconic"
- label="&zotero.contextMenu.saveLinkAsSnapshot;" hidden="true"
- oncommand="Zotero.Attachments.importFromURL(window.gContextMenu.linkURL, false, false, false, ZoteroPane.getSelectedCollection(true))"/>
- <menuitem id="zotero-context-save-image-as-snapshot" class="menu-iconic"
- label="&zotero.contextMenu.saveImageAsSnapshot;" hidden="true"
- oncommand="Zotero.Attachments.importFromURL(window.gContextMenu.onImage ? window.gContextMenu.imageURL : window.gContextMenu.bgImageURL, false, false, false, ZoteroPane.getSelectedCollection(true))"/>
+ <!-- TODO: localize and remove zotero.contextMenu.saveLinkAsItem/saveImageAsSnapshot -->
+ <menuitem id="zotero-context-save-link-as-item" class="menu-iconic"
+ label="Save Link as Zotero Item" hidden="true"
+ oncommand="ZoteroPane.addItemFromURL(window.gContextMenu.linkURL)"/>
+ <menuitem id="zotero-context-save-image-as-item" class="menu-iconic"
+ label="Save Image as Zotero Item" hidden="true"
+ oncommand="ZoteroPane.addItemFromURL(window.gContextMenu.onImage ? (window.gContextMenu.mediaURL ? window.gContextMenu.mediaURL : window.gContextMenu.imageURL) : window.gContextMenu.bgImageURL, 'artwork')"/>
</popup>
<vbox id="appcontent">
@@ -96,8 +97,8 @@
<menuitem label="&zotero.items.menu.showInLibrary;" oncommand="ZoteroPane.selectItem(this.parentNode.getAttribute('itemID'), true)"/>
<menuseparator/>
<menuitem label="&zotero.items.menu.attach.note;" oncommand="ZoteroPane.newNote(false, this.parentNode.getAttribute('itemID'))"/>
- <menuitem label="&zotero.items.menu.attach.snapshot;" oncommand="ZoteroPane.addAttachmentFromPage(false, this.parentNode.getAttribute('itemID'));"/>
- <menuitem label="&zotero.items.menu.attach.link;" oncommand="ZoteroPane.addAttachmentFromPage(true, this.parentNode.getAttribute('itemID'));"/>
+ <menuitem label="&zotero.items.menu.attach.snapshot;" oncommand="ZoteroPane.addAttachmentFromPage(false, parseInt(this.parentNode.getAttribute('itemID')))"/>
+ <menuitem label="&zotero.items.menu.attach.link;" oncommand="ZoteroPane.addAttachmentFromPage(true, parseInt(this.parentNode.getAttribute('itemID')))"/>
<menuseparator/>
<menuitem label="&zotero.items.menu.duplicateItem;" oncommand="ZoteroPane.duplicateSelectedItem();"/>
<menuitem oncommand="ZoteroPane.deleteSelectedItems();"/>
@@ -118,6 +119,7 @@
<vbox flex="1">
<hbox class="toolbar">
<toolbarbutton id="zotero-tb-collection-add" tooltiptext="&zotero.toolbar.newCollection.label;" oncommand="ZoteroPane.newCollection()"/>
+ <toolbarbutton id="zotero-tb-group-add" tooltiptext="&zotero.toolbar.newGroup;" oncommand="ZoteroPane.newGroup()"/>
<spacer flex="1"/>
<toolbarbutton id="zotero-tb-tag-selector" tooltiptext="&zotero.toolbar.tagSelector.label;" oncommand="ZoteroPane.toggleTagSelector()"/>
<toolbarbutton id="zotero-tb-actions-menu" tooltiptext="&zotero.toolbar.actions.label;" type="menu">
@@ -154,7 +156,7 @@
onmouseover="ZoteroPane.collectionsView.setHighlightedRows();"
ondblclick="ZoteroPane.onDoubleClick(event, this);"
onkeypress="ZoteroPane.handleKeyPress(event, this.id)"
- onselect="ZoteroPane.onCollectionSelected();" seltype="single"
+ onselect="ZoteroPane.onCollectionSelected();" seltype="cell"
ondraggesture="if (event.target.localName == 'treechildren') { ZoteroPane.startDrag(event, ZoteroPane.collectionsView); }"
ondragenter="return ZoteroPane.dragEnter(event, ZoteroPane.collectionsView)"
ondragover="return ZoteroPane.dragOver(event, ZoteroPane.collectionsView)"
@@ -192,10 +194,12 @@
</menupopup>
</toolbarbutton>
<toolbarbutton id="zotero-tb-item-from-page" tooltiptext="&zotero.toolbar.newItemFromPage.label;" oncommand="ZoteroPane.addItemFromPage()"/>
- <toolbarbutton id="zotero-tb-lookup" tooltiptext="&zotero.toolbar.lookup.label;" oncommand="window.openDialog('chrome://zotero/content/lookup.xul', 'lookup', 'chrome,modal')"/>
+ <toolbarbutton id="zotero-tb-lookup" tooltiptext="&zotero.toolbar.lookup.label;" oncommand="ZoteroPane.openLookupWindow()"/>
+ <!--
<toolbarseparator/>
<toolbarbutton id="zotero-tb-link-page" tooltiptext="&zotero.toolbar.attachment.weblink;" oncommand="ZoteroPane.addAttachmentFromPage(true)"/>
<toolbarbutton id="zotero-tb-snapshot-page" tooltiptext="&zotero.toolbar.attachment.snapshot;" oncommand="ZoteroPane.addAttachmentFromPage()"/>
+ -->
<toolbarbutton id="zotero-tb-note-add" tooltiptext="&zotero.toolbar.note.standalone;" oncommand="ZoteroPane.newNote(event.shiftKey);"/>
<toolbarseparator/>
<toolbarbutton id="zotero-tb-advanced-search" tooltiptext="&zotero.toolbar.advancedSearch;" oncommand="ZoteroPane.openAdvancedSearchWindow()"/>
@@ -406,7 +410,7 @@
<!-- Scrape Code -->
<hbox id="urlbar-icons">
- <image src="chrome://zotero/skin/treeitem-book.png" id="zotero-status-image" onclick="Zotero_Browser.scrapeThisPage()" position="1" hidden="true"/>
+ <image src="chrome://zotero/skin/treeitem-book.png" id="zotero-status-image" onclick="Zotero_Browser.scrapeThisPage(ZoteroPane.getSelectedLibraryID(), ZoteroPane.getSelectedCollection(true))" position="1" hidden="true"/>
</hbox>
<statusbar id="status-bar">
diff --git a/chrome/content/zotero/preferences/preferences.js b/chrome/content/zotero/preferences/preferences.js
@@ -329,8 +329,7 @@ function handleSyncReset(action) {
var appStartup = Components.classes["@mozilla.org/toolkit/app-startup;1"]
.getService(Components.interfaces.nsIAppStartup);
- appStartup.quit(Components.interfaces.nsIAppStartup.eRestart);
- appStartup.quit(Components.interfaces.nsIAppStartup.eAttemptQuit);
+ appStartup.quit(Components.interfaces.nsIAppStartup.eRestart | Components.interfaces.nsIAppStartup.eAttemptQuit);
};
// TODO: better way of checking for an active session?
diff --git a/chrome/content/zotero/preferences/preferences.xul b/chrome/content/zotero/preferences/preferences.xul
@@ -244,11 +244,10 @@ To add a new preference:
<groupbox>
<caption label="Storage Server"/>
- <hbox>
- <checkbox label="Enable file syncing" preference="pref-storage-enabled"/>
- </hbox>
-
<separator class="thin"/>
+ <label value="Please note: Attachment files in group libraries are not currently synced."/>
+
+ <separator/>
<grid id="storage-settings">
<columns>
@@ -714,8 +713,8 @@ To add a new preference:
<!-- These mess up the prefwindow (more) if they come before the prefpanes
https://bugzilla.mozilla.org/show_bug.cgi?id=296418 -->
- <script src="chrome://zotero/content/include.js"/>
- <script src="chrome://zotero/content/charsetMenu.js"/>
+ <script src="chrome://zotero/content/include.js"></script>
+ <script src="chrome://zotero/content/charsetMenu.js"></script>
<script type="application/javascript">
<![CDATA[
var observerService = Components.classes["@mozilla.org/observer-service;1"].getService(Components.interfaces.nsIObserverService);
diff --git a/chrome/content/zotero/selectItemsDialog.xul b/chrome/content/zotero/selectItemsDialog.xul
@@ -53,7 +53,7 @@
<hbox flex="1">
<tree id="zotero-collections-tree"
- style="width: 200px;" hidecolumnpicker="true" seltype="single"
+ style="width: 200px;" hidecolumnpicker="true" seltype="cell"
onselect="onCollectionSelected();">
<treecols>
<treecol
diff --git a/chrome/content/zotero/xpcom/attachments.js b/chrome/content/zotero/xpcom/attachments.js
@@ -56,7 +56,11 @@ Zotero.Attachments = new function(){
try {
// Create a new attachment
- var attachmentItem = new Zotero.Item(false, 'attachment');
+ var attachmentItem = new Zotero.Item('attachment');
+ if (sourceItemID) {
+ var parentItem = Zotero.Items.get(sourceItemID);
+ attachmentItem.libraryID = parentItem.libraryID;
+ }
attachmentItem.setField('title', title);
attachmentItem.setSource(sourceItemID);
attachmentItem.attachmentLinkMode = this.LINK_MODE_IMPORTED_FILE;
@@ -128,7 +132,11 @@ Zotero.Attachments = new function(){
try {
// Create a new attachment
- var attachmentItem = new Zotero.Item(false, 'attachment');
+ var attachmentItem = new Zotero.Item('attachment');
+ if (sourceItemID) {
+ var parentItem = Zotero.Items.get(sourceItemID);
+ attachmentItem.libraryID = parentItem.libraryID;
+ }
attachmentItem.setField('title', title);
attachmentItem.setField('url', url);
attachmentItem.setSource(sourceItemID);
@@ -182,6 +190,13 @@ Zotero.Attachments = new function(){
function importFromURL(url, sourceItemID, forceTitle, forceFileBaseName, parentCollectionIDs){
Zotero.debug('Importing attachment from URL');
+ if (sourceItemID && parentCollectionIDs) {
+ var msg = "parentCollectionIDs is ignored when sourceItemID is set in Zotero.Attachments.importFromURL()";
+ Zotero.debug(msg, 2);
+ Components.utils.reportError(msg);
+ parentCollectionIDs = undefined;
+ }
+
// Throw error on invalid URLs
//
// TODO: allow other schemes
@@ -264,7 +279,11 @@ Zotero.Attachments = new function(){
try {
// Create a new attachment
- var attachmentItem = new Zotero.Item(false, 'attachment');
+ var attachmentItem = new Zotero.Item('attachment');
+ if (sourceItemID) {
+ var parentItem = Zotero.Items.get(sourceItemID);
+ attachmentItem.libraryID = parentItem.libraryID;
+ }
attachmentItem.setField('title', title);
attachmentItem.setField('url', url);
attachmentItem.setField('accessDate', "CURRENT_TIMESTAMP");
@@ -292,6 +311,7 @@ Zotero.Attachments = new function(){
wbp.progressListener = new Zotero.WebProgressFinishListener(function(){
try {
var str = Zotero.File.getSample(file);
+
if (mimeType == 'application/pdf' &&
Zotero.MIME.sniffForMIMEType(str) != 'application/pdf') {
Zotero.debug("Downloaded PDF did not have MIME type "
@@ -432,6 +452,13 @@ Zotero.Attachments = new function(){
function linkFromDocument(document, sourceItemID, parentCollectionIDs){
Zotero.debug('Linking attachment from document');
+ if (sourceItemID && parentCollectionIDs) {
+ var msg = "parentCollectionIDs is ignored when sourceItemID is set in Zotero.Attachments.linkFromDocument()";
+ Zotero.debug(msg, 2);
+ Components.utils.reportError(msg);
+ parentCollectionIDs = undefined;
+ }
+
var url = document.location.href;
var title = document.title; // TODO: don't use Mozilla-generated title for images, etc.
var mimeType = document.contentType;
@@ -475,6 +502,13 @@ Zotero.Attachments = new function(){
function importFromDocument(document, sourceItemID, forceTitle, parentCollectionIDs, callback) {
Zotero.debug('Importing attachment from document');
+ if (sourceItemID && parentCollectionIDs) {
+ var msg = "parentCollectionIDs is ignored when sourceItemID is set in Zotero.Attachments.importFromDocument()";
+ Zotero.debug(msg, 2);
+ Components.utils.reportError(msg);
+ parentCollectionIDs = undefined;
+ }
+
var url = document.location.href;
var title = forceTitle ? forceTitle : document.title;
var mimeType = document.contentType;
@@ -496,7 +530,11 @@ Zotero.Attachments = new function(){
try {
// Create a new attachment
- var attachmentItem = new Zotero.Item(false, 'attachment');
+ var attachmentItem = new Zotero.Item('attachment');
+ if (sourceItemID) {
+ var parentItem = Zotero.Items.get(sourceItemID);
+ attachmentItem.libraryID = parentItem.libraryID;
+ }
attachmentItem.setField('title', title);
attachmentItem.setField('url', url);
attachmentItem.setField('accessDate', "CURRENT_TIMESTAMP");
@@ -679,7 +717,7 @@ Zotero.Attachments = new function(){
try {
// Create a new attachment
- var attachmentItem = new Zotero.Item(false, 'attachment');
+ var attachmentItem = new Zotero.Item('attachment');
attachmentItem.setField('title', title);
attachmentItem.setField('url', url);
attachmentItem.setField('accessDate', "CURRENT_TIMESTAMP");
@@ -1118,7 +1156,14 @@ Zotero.Attachments = new function(){
function _addToDB(file, url, title, linkMode, mimeType, charsetID, sourceItemID) {
Zotero.DB.beginTransaction();
- var attachmentItem = new Zotero.Item(false, 'attachment');
+ var attachmentItem = new Zotero.Item('attachment');
+ if (sourceItemID) {
+ var parentItem = Zotero.Items.get(sourceItemID);
+ if (parentItem.libraryID && linkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE) {
+ throw ("Cannot save linked file in non-local library");
+ }
+ attachmentItem.libraryID = parentItem.libraryID;
+ }
attachmentItem.setField('title', title);
if (linkMode == self.LINK_MODE_IMPORTED_URL
|| linkMode == self.LINK_MODE_LINKED_URL) {
diff --git a/chrome/content/zotero/xpcom/collectionTreeView.js b/chrome/content/zotero/xpcom/collectionTreeView.js
@@ -36,7 +36,7 @@ Zotero.CollectionTreeView = function()
this._treebox = null;
this.itemToSelect = null;
this._highlightedRows = {};
- this._unregisterID = Zotero.Notifier.registerObserver(this, ['collection', 'search', 'share']);
+ this._unregisterID = Zotero.Notifier.registerObserver(this, ['collection', 'search', 'share', 'group']);
this.showDuplicates = false;
}
@@ -72,7 +72,7 @@ Zotero.CollectionTreeView.prototype.setTree = function(treebox)
// Select the last-viewed collection
var lastViewedFolder = Zotero.Prefs.get('lastViewedFolder');
- var matches = lastViewedFolder.match(/^(?:(C|S)([0-9]+)|L)$/);
+ var matches = lastViewedFolder.match(/^(?:(C|S|G)([0-9]+)|L)$/);
var select = 0;
if (matches) {
if (matches[1] == 'C') {
@@ -131,7 +131,12 @@ Zotero.CollectionTreeView.prototype.setTree = function(treebox)
else if (matches[1] == 'S' && this._searchRowMap[matches[2]]) {
select = this._searchRowMap[matches[2]];
}
+ else if (matches[1] == 'G' && this._groupRowMap[matches[2]]) {
+ select = this._groupRowMap[matches[2]];
+ }
}
+
+ this.selection.currentColumn = this._treebox.columns.getFirstColumn();
this.selection.select(select);
}
@@ -145,32 +150,67 @@ Zotero.CollectionTreeView.prototype.refresh = function()
var oldCount = this.rowCount;
this._dataItems = [];
this.rowCount = 0;
- this._showItem(new Zotero.ItemGroup('library',null),0,1);
+ this._showItem(new Zotero.ItemGroup('library', { id: null, libraryID: null }), 0, 1, 1); // itemgroup ref, level, beforeRow, startOpen
- var newRows = Zotero.getCollections();
- for(var i = 0; i < newRows.length; i++)
- this._showItem(new Zotero.ItemGroup('collection',newRows[i]), 0, this._dataItems.length); //itemgroup ref, level, beforeRow
+ var collections = Zotero.getCollections();
+ for (var i=0; i<collections.length; i++) {
+ // Skip group collections
+ if (collections[i].libraryID) {
+ continue;
+ }
+ this._showItem(new Zotero.ItemGroup('collection', collections[i]), 1);
+ }
var savedSearches = Zotero.Searches.getAll();
if (savedSearches) {
for (var i=0; i<savedSearches.length; i++) {
- this._showItem(new Zotero.ItemGroup('search',savedSearches[i]), 0, this._dataItems.length); //itemgroup ref, level, beforeRow
- }
- }
-
- var shares = Zotero.Zeroconf.instances;
- if (shares) {
- for each(var share in shares) {
- this._showItem(new Zotero.ItemGroup('share', share), 0, this._dataItems.length); //itemgroup ref, level, beforeRow
+ this._showItem(new Zotero.ItemGroup('search', savedSearches[i]), 1);
}
}
var deletedItems = Zotero.Items.getDeleted();
if (deletedItems || Zotero.Prefs.get("showTrashWhenEmpty")) {
- this._showItem(new Zotero.ItemGroup('trash', null), 0, this._dataItems.length);
+ this._showItem(new Zotero.ItemGroup('trash', false), 1);
}
this.trashNotEmpty = !!deletedItems;
+ var groups = Zotero.Groups.getAll();
+ if (groups.length) {
+ this._showItem(new Zotero.ItemGroup('separator', false));
+ var self = this;
+ var header = {
+ id: "group-libraries-header",
+ label: "Group Libraries", // TODO: localize
+ expand: function (groups) {
+ if (!groups) {
+ var groups = Zotero.Groups.getAll();
+ }
+
+ for (var i=0; i<groups.length; i++) {
+ var startOpen = groups[i].hasCollections();
+
+ self._showItem(new Zotero.ItemGroup('group', groups[i]), 1, null, startOpen);
+
+ // Add group collections
+ var collections = groups[i].getCollections();
+ for (var j=0; j<collections.length; j++) {
+ self._showItem(new Zotero.ItemGroup('collection', collections[j]), 2);
+ }
+ }
+ }
+ };
+ this._showItem(new Zotero.ItemGroup('header', header), null, null, true);
+ header.expand(groups);
+ }
+
+ var shares = Zotero.Zeroconf.instances;
+ if (shares.length) {
+ this._showItem(new Zotero.ItemGroup('separator', false));
+ for each(var share in shares) {
+ this._showItem(new Zotero.ItemGroup('share', share));
+ }
+ }
+
this._refreshHashMap();
// Update the treebox's row count
@@ -248,6 +288,12 @@ Zotero.CollectionTreeView.prototype.notify = function(action, type, ids)
rows.push(this._searchRowMap[ids[i]]);
}
break;
+
+ case 'group':
+ if (this._groupRowMap[ids[i]] != null) {
+ rows.push(this._groupRowMap[ids[i]]);
+ }
+ break;
}
}
@@ -303,13 +349,27 @@ Zotero.CollectionTreeView.prototype.notify = function(action, type, ids)
}
this.reload();
+ if (Zotero.Sync.Server.syncInProgress) {
+ this.rememberSelection(savedSelection);
+ break;
+ }
this.selection.select(this._collectionRowMap[collectionID]);
break;
case 'search':
this.reload();
+ if (Zotero.Sync.Server.syncInProgress) {
+ this.rememberSelection(savedSelection);
+ break;
+ }
this.selection.select(this._searchRowMap[ids]);
break;
+
+ case 'group':
+ this.reload();
+ // Groups can only be created during sync
+ this.rememberSelection(savedSelection);
+ break;
}
}
@@ -318,7 +378,7 @@ Zotero.CollectionTreeView.prototype.notify = function(action, type, ids)
/*
- * Set the rows that should be highlighted -- actually highlighting is done
+ * Set the rows that should be highlighted -- actual highlighting is done
* by getRowProperties based on the array set here
*/
Zotero.CollectionTreeView.prototype.setHighlightedRows = function (ids) {
@@ -376,16 +436,36 @@ Zotero.CollectionTreeView.prototype.getCellText = function(row, column)
Zotero.CollectionTreeView.prototype.getImageSrc = function(row, col)
{
- var collectionType = this._getItemAtRow(row).type;
- if (collectionType == 'trash' && this.trashNotEmpty) {
- collectionType += "-full";
+ var source = this._getItemAtRow(row);
+ var collectionType = source.type;
+ switch (collectionType) {
+ case 'trash':
+ if (this.trashNotEmpty) {
+ collectionType += '-full';
+ }
+ break;
+
+ case 'collection':
+ // TODO: group collection
+ break;
+
+ case 'header':
+ if (source.ref.id == 'group-libraries-header') {
+ collectionType = 'groups';
+ }
+ break;
+
+ case 'group':
+ collectionType = 'library';
+ break;
}
return "chrome://zotero/skin/treesource-" + collectionType + ".png";
}
Zotero.CollectionTreeView.prototype.isContainer = function(row)
{
- return this._getItemAtRow(row).isCollection();
+ var itemGroup = this._getItemAtRow(row);
+ return itemGroup.isLibrary(true) || itemGroup.isCollection() || itemGroup.isHeader();
}
Zotero.CollectionTreeView.prototype.isContainerOpen = function(row)
@@ -399,6 +479,15 @@ Zotero.CollectionTreeView.prototype.isContainerOpen = function(row)
Zotero.CollectionTreeView.prototype.isContainerEmpty = function(row)
{
var itemGroup = this._getItemAtRow(row);
+ if (itemGroup.isLibrary()) {
+ return false;
+ }
+ if (itemGroup.isHeader()) {
+ return false;
+ }
+ if (itemGroup.isGroup()) {
+ return !itemGroup.ref.hasCollections();
+ }
if (itemGroup.isCollection()) {
return !itemGroup.ref.hasChildCollections();
}
@@ -440,22 +529,30 @@ Zotero.CollectionTreeView.prototype.toggleOpenState = function(row)
var thisLevel = this.getLevel(row);
this._treebox.beginUpdateBatch();
- if(this.isContainerOpen(row))
- {
+ if (this.isContainerOpen(row)) {
while((row + 1 < this._dataItems.length) && (this.getLevel(row + 1) > thisLevel))
{
this._hideItem(row+1);
count--; //count is negative when closing a container because we are removing rows
}
}
- else
- {
- var newRows = Zotero.getCollections(this._getItemAtRow(row).ref.id); //Get children
+ else {
+ var itemGroup = this._getItemAtRow(row);
- for(var i = 0; i < newRows.length; i++)
- {
- count++;
- this._showItem(new Zotero.ItemGroup('collection',newRows[i]), thisLevel+1, row+i+1); //insert new row
+ if (itemGroup.type == 'header') {
+ itemGroup.ref.expand();
+ }
+ else {
+ if (itemGroup.isGroup()) {
+ var collections = itemGroup.ref.getCollections(); // Get child collections
+ }
+ else {
+ var collections = Zotero.getCollections(itemGroup.ref.id); // Get child collections
+ }
+ for (var i=0; i<collections.length; i++) {
+ count++;
+ this._showItem(new Zotero.ItemGroup('collection', collections[i]), thisLevel+1, row+i+1); //insert new row
+ }
}
}
this._dataItems[row][1] = !this._dataItems[row][1]; //toggle container open value
@@ -467,6 +564,21 @@ Zotero.CollectionTreeView.prototype.toggleOpenState = function(row)
}
+Zotero.CollectionTreeView.prototype.isSelectable = function (row, col) {
+ var itemGroup = this._getItemAtRow(row);
+ switch (itemGroup.type) {
+ case 'separator':
+ return false;
+ }
+ return true;
+}
+
+
+Zotero.CollectionTreeView.prototype.__defineGetter__('editable', function () {
+ return this._getItemAtRow(this.selection.currentIndex).isEditable();
+});
+
+
Zotero.CollectionTreeView.prototype.expandAllRows = function(treebox) {
var view = treebox.view;
treebox.beginUpdateBatch();
@@ -522,6 +634,39 @@ Zotero.CollectionTreeView.prototype.collapseAllRows = function(treebox) {
/// Additional functions for managing data in the tree
///
////////////////////////////////////////////////////////////////////////////////
+/**
+ * @param {Integer|null} libraryID Library to select, or null for local library
+ */
+Zotero.CollectionTreeView.prototype.selectLibrary = function (libraryID) {
+ if (Zotero.Sync.Server.syncInProgress) {
+ Zotero.debug("Sync in progress -- not changing library selection");
+ return false;
+ }
+
+ // Select local library
+ if (!libraryID) {
+ this.selection.select(0);
+ return true;
+ }
+
+ // Already selected
+ var itemGroup = this._getItemAtRow(this.selection.currentIndex);
+ if (itemGroup.ref.libraryID == libraryID) {
+ return true;
+ }
+
+ // Find library
+ for (var i=0, rows=this.rowCount; i<rows.length; i++) {
+ var itemGroup = this._getItemAtRow(this.selection.currentIndex);
+ if (itemGroup.ref && itemGroup.ref.libraryID == libraryID) {
+ this.selection.select(i);
+ return true;
+ }
+ }
+
+ return false;
+}
+
/*
* Delete the selection
@@ -579,9 +724,21 @@ Zotero.CollectionTreeView.prototype.deleteSelection = function()
* level: the indent level of the row
* beforeRow: row index to insert new row before
*/
-Zotero.CollectionTreeView.prototype._showItem = function(itemGroup, level, beforeRow)
+Zotero.CollectionTreeView.prototype._showItem = function(itemGroup, level, beforeRow, startOpen)
{
- this._dataItems.splice(beforeRow, 0, [itemGroup, false, level]);
+ if (!level) {
+ level = 0;
+ }
+
+ if (!beforeRow) {
+ beforeRow = this._dataItems.length;
+ }
+
+ if (!startOpen) {
+ startOpen = false;
+ }
+
+ this._dataItems.splice(beforeRow, 0, [itemGroup, startOpen, level]);
this.rowCount++;
}
@@ -609,18 +766,22 @@ Zotero.CollectionTreeView.prototype.saveSelection = function()
{
for (var i=0, len=this.rowCount; i<len; i++) {
if (this.selection.isSelected(i)) {
- if (this._getItemAtRow(i).isLibrary()) {
+ var itemGroup = this._getItemAtRow(i);
+ if (itemGroup.isLibrary()) {
return 'L';
}
- else if (this._getItemAtRow(i).isCollection()) {
- return 'C' + this._getItemAtRow(i).ref.id;
+ else if (itemGroup.isCollection()) {
+ return 'C' + itemGroup.ref.id;
}
- else if (this._getItemAtRow(i).isSearch()) {
- return 'S' + this._getItemAtRow(i).ref.id;
+ else if (itemGroup.isSearch()) {
+ return 'S' + itemGroup.ref.id;
}
- else if (this._getItemAtRow(i).isTrash()) {
+ else if (itemGroup.isTrash()) {
return 'T';
}
+ else if (itemGroup.isGroup()) {
+ return 'G' + itemGroup.ref.id;
+ }
}
}
return false;
@@ -658,6 +819,7 @@ Zotero.CollectionTreeView.prototype.rememberSelection = function(selection)
}
break;
+ // Trash
case 'T':
if (this._getItemAtRow(this.rowCount-1).isTrash()){
this.selection.select(this.rowCount-1);
@@ -666,6 +828,13 @@ Zotero.CollectionTreeView.prototype.rememberSelection = function(selection)
this.selection.select(0);
}
break;
+
+ // Group
+ case 'G':
+ if (this._groupRowMap[id] != undefined) {
+ this.selection.select(this._groupRowMap[i]);
+ }
+ break;
}
}
@@ -679,12 +848,17 @@ Zotero.CollectionTreeView.prototype._refreshHashMap = function()
{
this._collectionRowMap = [];
this._searchRowMap = [];
+ this._groupRowMap = [];
for(var i=0; i < this.rowCount; i++){
- if (this.isCollection(i)){
- this._collectionRowMap[this._getItemAtRow(i).ref.id] = i;
+ var itemGroup = this._getItemAtRow(i);
+ if (itemGroup.isCollection(i)) {
+ this._collectionRowMap[itemGroup.ref.id] = i;
+ }
+ else if (itemGroup.isSearch(i)) {
+ this._searchRowMap[itemGroup.ref.id] = i;
}
- else if (this.isSearch(i)){
- this._searchRowMap[this._getItemAtRow(i).ref.id] = i;
+ else if (itemGroup.isGroup(i)) {
+ this._groupRowMap[itemGroup.ref.id] = i;
}
}
}
@@ -730,7 +904,11 @@ Zotero.CollectionTreeCommandController.prototype.onEvent = function(evt)
* Start a drag using nsDragAndDrop.js or HTML 5 Drag and Drop
*/
Zotero.CollectionTreeView.prototype.onDragStart = function(event, transferData, action) {
- var collectionID = this._getItemAtRow(this.selection.currentIndex).ref.id;
+ var itemGroup = this._getItemAtRow(this.selection.currentIndex);
+ if (!itemGroup.isCollection()) {
+ return false;
+ }
+ var collectionID = itemGroup.ref.id;
// Use nsDragAndDrop.js interface for Firefox 2 and Firefox 3.0
var oldMethod = Zotero.isFx2 || Zotero.isFx30;
@@ -789,22 +967,56 @@ Zotero.CollectionTreeView.prototype.canDrop = function(row, orient, dragData)
}
else if(orient == 0) //directly on a row...
{
- var rowCollection = this._getItemAtRow(row).ref; //the collection we are dragging over
+ var itemGroup = this._getItemAtRow(row); //the collection we are dragging over
+
+ if (!itemGroup.isEditable()) {
+ return false;
+ }
if (dataType == 'zotero/item') {
var ids = data;
- for each(var id in ids)
- {
- var item = Zotero.Items.get(id);
- // Can only drag top-level items into collections
- if (item.isRegularItem() || !item.getSource())
- {
- // Make sure there's at least one item that's not already
- // in this collection
- if (!rowCollection.hasItem(id))
- {
- return true;
+ var items = Zotero.Items.get(ids);
+ for each(var item in items) {
+ // Can only drag top-level items
+ if (!(item.isRegularItem() || !item.getSource())) {
+ continue;
+ }
+
+ // TODO: for now, only allow regular items to be dragged to groups
+ if (itemGroup.isWithinGroup() && itemGroup.ref.libraryID != item.libraryID
+ && !item.isRegularItem()) {
+ return false;
+ }
+
+ // TODO: for now, skip items that are already linked
+ if (itemGroup.isWithinGroup() && itemGroup.ref.libraryID != item.libraryID) {
+ if (item.getLinkedItem(itemGroup.ref.libraryID)) {
+ continue;
+ }
+ }
+
+ if (itemGroup.isGroup()) {
+ // Don't allow drag onto library of same group
+ if (itemGroup.ref.libraryID == item.libraryID) {
+ continue;
}
+ return true;
+ }
+
+ // Allow drag of group items to library
+ if (item.libraryID && (itemGroup.isLibrary()
+ || itemGroup.isCollection() && !itemGroup.isWithinGroup())) {
+ // TODO: for now, skip items that are already linked
+ if (item.getLinkedItem()) {
+ continue;
+ }
+ return true;
+ }
+
+ // Make sure there's at least one item that's not already
+ // in this collection
+ if (itemGroup.isCollection() && !itemGroup.ref.hasItem(item.id)) {
+ return true;
}
}
return false;
@@ -819,9 +1031,8 @@ Zotero.CollectionTreeView.prototype.canDrop = function(row, orient, dragData)
}
return false;
}
- else if (dataType == 'text/x-moz-url'
- || dataType == 'application/x-moz-file') {
- if (this._getItemAtRow(row).isSearch()) {
+ else if (dataType == 'text/x-moz-url' || dataType == 'application/x-moz-file') {
+ if (itemGroup.isSearch()) {
return false;
}
// Don't allow folder drag
@@ -830,10 +1041,30 @@ Zotero.CollectionTreeView.prototype.canDrop = function(row, orient, dragData)
}
return true;
}
- else if (dataType == 'zotero/collection'
- // Collections cannot be dropped on themselves, nor in their children
- && data[0] != rowCollection.id
- && !Zotero.Collections.get(data[0]).hasDescendent('collection', rowCollection.id)) {
+ else if (dataType == 'zotero/collection') {
+ // Collections cannot be dropped on themselves
+ if (data[0] == itemGroup.ref.id) {
+ return false;
+ }
+
+ // Nor in their children
+ if (Zotero.Collections.get(data[0]).hasDescendent('collection', itemGroup.ref.id)) {
+ return false;
+ }
+
+ var col = Zotero.Collections.get(data[0]);
+
+ // Nor, at least for now, on another group
+ if (itemGroup.isWithinGroup()) {
+ if (itemGroup.ref.libraryID != col.libraryID) {
+ return false;
+ }
+ }
+ // Nor from a group library to the local library
+ else if (col.libraryID) {
+ return false;
+ }
+
return true;
}
}
@@ -853,12 +1084,14 @@ Zotero.CollectionTreeView.prototype.drop = function(row, orient)
var dataType = dragData.dataType;
var data = dragData.data;
+ var itemGroup = this._getItemAtRow(row);
if(dataType == 'zotero/collection')
{
var targetCollectionID;
- if(this._getItemAtRow(row).isCollection())
- targetCollectionID = this._getItemAtRow(row).ref.id;
+ if (itemGroup.isCollection()) {
+ targetCollectionID = itemGroup.ref.id;
+ }
var droppedCollection = Zotero.Collections.get(data[0]);
droppedCollection.parent = targetCollectionID;
droppedCollection.save();
@@ -869,18 +1102,150 @@ Zotero.CollectionTreeView.prototype.drop = function(row, orient)
return;
}
- var toAdd = [];
- for (var i=0; i<ids.length; i++) {
- var item = Zotero.Items.get(ids[i]);
- // Only accept top-level items
- if (item.isRegularItem() || !item.getSource()) {
- toAdd.push(ids[i]);
+ if (itemGroup.isWithinGroup()) {
+ var targetLibraryID = itemGroup.ref.libraryID;
+ }
+ else {
+ var targetLibraryID = null;
+ }
+
+ Zotero.DB.beginTransaction();
+
+ var items = Zotero.Items.get(ids);
+ if (!items) {
+ return;
+ }
+
+ var newItems = [];
+ var newIDs = [];
+ // DEBUG: support items coming from different sources?
+ if (items[0].libraryID == targetLibraryID) {
+ var sameLibrary = true;
+ }
+ else {
+ var sameLibrary = false;
+ }
+
+ for each(var item in items) {
+ if (!(item.isRegularItem() || !item.getSource())) {
+ continue;
+ }
+
+ if (sameLibrary) {
+ newIDs.push(item.id);
+ }
+ else {
+ newItems.push(item);
}
}
- if (toAdd.length > 0) {
- this._getItemAtRow(row).ref.addItems(toAdd);
+ if (!sameLibrary) {
+ var toReconcile = [];
+
+ for each(var item in newItems) {
+ // Check if there's already a copy of this item in the library
+ var linkedItem = item.getLinkedItem(targetLibraryID);
+ if (linkedItem) {
+ Zotero.debug("Linked item already exists -- skipping");
+ continue;
+
+ /*
+ // TODO: support tags, related, attachments, etc.
+
+ // Overlay source item fields on unsaved clone of linked item
+ var newItem = item.clone(false, linkedItem.clone(true));
+ newItem.setField('dateAdded', item.dateAdded);
+ newItem.setField('dateModified', item.dateModified);
+
+ var diff = newItem.diff(linkedItem, false, ["dateAdded", "dateModified"]);
+ if (!diff) {
+ // Check if creators changed
+ var creatorsChanged = false;
+
+ var creators = item.getCreators();
+ var linkedCreators = linkedItem.getCreators();
+ if (creators.length != linkedCreators.length) {
+ Zotero.debug('Creators have changed');
+ creatorsChanged = true;
+ }
+ else {
+ for (var i=0; i<creators.length; i++) {
+ if (!creators[i].ref.equals(linkedCreators[i].ref)) {
+ Zotero.debug('changed');
+ creatorsChanged = true;
+ break;
+ }
+ }
+ }
+ if (!creatorsChanged) {
+ Zotero.debug("Linked item hasn't changed -- skipping conflict resolution");
+ continue;
+ }
+ }
+ toReconcile.push([newItem, linkedItem]);
+ continue;
+ */
+ }
+
+ // Create new unsaved clone item in target library
+ var newItem = new Zotero.Item(item.itemTypeID);
+ newItem.libraryID = targetLibraryID;
+ // DEBUG: save here because clone() doesn't currently work on unsaved tagged items
+ var id = newItem.save();
+ var newItem = Zotero.Items.get(id);
+ item.clone(false, newItem);
+ newItem.save();
+ //var id = newItem.save();
+ //var newItem = Zotero.Items.get(id);
+
+ // Record link
+ item.addLinkedItem(newItem);
+ newIDs.push(id);
+ }
+
+ if (toReconcile.length) {
+ var sourceName = items[0].libraryID ? Zotero.Libraries.getName(items[0].libraryID)
+ : Zotero.getString('pane.collections.library');
+ var targetName = targetLibraryID ? Zotero.Libraries.getName(libraryID)
+ : Zotero.getString('pane.collections.library');
+
+ var io = {
+ dataIn: {
+ type: "item",
+ captions: [
+ // TODO: localize
+ sourceName,
+ targetName,
+ "Merged Item"
+ ],
+ objects: toReconcile
+ }
+ };
+
+ /*
+ if (type == 'item') {
+ if (!Zotero.Utilities.prototype.isEmpty(changedCreators)) {
+ io.dataIn.changedCreators = changedCreators;
+ }
+ }
+ */
+
+ var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
+ .getService(Components.interfaces.nsIWindowMediator);
+ var lastWin = wm.getMostRecentWindow("navigator:browser");
+ lastWin.openDialog('chrome://zotero/content/merge.xul', '', 'chrome,modal,centerscreen', io);
+
+ for each(var obj in io.dataOut) {
+ obj.ref.save();
+ }
+ }
+ }
+
+ if (newIDs.length && itemGroup.isCollection()) {
+ itemGroup.ref.addItems(newIDs);
}
+
+ Zotero.DB.commitTransaction();
}
else if (dataType == 'zotero/item-xml') {
Zotero.DB.beginTransaction();
@@ -901,8 +1266,16 @@ Zotero.CollectionTreeView.prototype.drop = function(row, orient)
return;
}
else if (dataType == 'text/x-moz-url' || dataType == 'application/x-moz-file') {
- if (this._getItemAtRow(row).isCollection()) {
- var parentCollectionID = this._getItemAtRow(row).ref.id;
+ // FIXME: temporarily disable dragging in of files
+ if (dataType == 'application/x-moz-file' && itemGroup.isWithinGroup()) {
+ var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
+ .getService(Components.interfaces.nsIPromptService);
+ ps.alert(null, "", "Files cannot currently be added to group libraries.");
+ return;
+ }
+
+ if (itemGroup.isCollection()) {
+ var parentCollectionID = itemGroup.ref.id;
}
else {
var parentCollectionID = false;
@@ -972,7 +1345,7 @@ Zotero.CollectionTreeView.prototype.drop = function(row, orient)
* Called by HTML 5 Drag and Drop when dragging over the tree
*/
Zotero.CollectionTreeView.prototype.onDragEnter = function (event) {
- Zotero.debug("Storing current drag data");
+ //Zotero.debug("Storing current drag data");
Zotero.DragDrop.currentDataTransfer = event.dataTransfer;
}
@@ -993,7 +1366,7 @@ Zotero.CollectionTreeView.prototype.onDrop = function (event, dropdata, session)
}
Zotero.CollectionTreeView.prototype.onDragExit = function (event) {
- Zotero.debug("Clearing drag data");
+ //Zotero.debug("Clearing drag data");
Zotero.DragDrop.currentDataTransfer = null;
}
@@ -1007,7 +1380,6 @@ Zotero.CollectionTreeView.prototype.onDragExit = function (event) {
////////////////////////////////////////////////////////////////////////////////
Zotero.CollectionTreeView.prototype.isSorted = function() { return false; }
-Zotero.CollectionTreeView.prototype.isSeparator = function(row) { return false; }
Zotero.CollectionTreeView.prototype.isEditable = function(row, idx) { return false; }
/* Set 'highlighted' property on rows set by setHighlightedRows */
@@ -1021,6 +1393,10 @@ Zotero.CollectionTreeView.prototype.getRowProperties = function(row, props) {
Zotero.CollectionTreeView.prototype.getColumnProperties = function(col, prop) { }
Zotero.CollectionTreeView.prototype.getCellProperties = function(row, col, prop) { }
+Zotero.CollectionTreeView.prototype.isSeparator = function(index) {
+ var source = this._getItemAtRow(index);
+ return source.type == 'separator';
+}
Zotero.CollectionTreeView.prototype.performAction = function(action) { }
Zotero.CollectionTreeView.prototype.performActionOnCell = function(action, row, col) { }
Zotero.CollectionTreeView.prototype.getProgressMode = function(row, col) { }
@@ -1039,8 +1415,11 @@ Zotero.ItemGroup = function(type, ref)
this.ref = ref;
}
-Zotero.ItemGroup.prototype.isLibrary = function()
+Zotero.ItemGroup.prototype.isLibrary = function(includeGlobal)
{
+ if (includeGlobal) {
+ return this.type == 'library' || this.type == 'group';
+ }
return this.type == 'library';
}
@@ -1064,37 +1443,100 @@ Zotero.ItemGroup.prototype.isTrash = function()
return this.type == 'trash';
}
+Zotero.ItemGroup.prototype.isGroup = function() {
+ return this.type == 'group';
+}
-Zotero.ItemGroup.prototype.getName = function()
-{
- if (this.isCollection()) {
- return this.ref.name;
- }
- else if (this.isLibrary()) {
- return Zotero.getString('pane.collections.library');
+Zotero.ItemGroup.prototype.isHeader = function () {
+ return this.type == 'header';
+}
+
+Zotero.ItemGroup.prototype.isSeparator = function () {
+ return this.type == 'separator';
+}
+
+
+// Special
+Zotero.ItemGroup.prototype.isWithinGroup = function () {
+ return this.ref && !!this.ref.libraryID;
+}
+
+Zotero.ItemGroup.prototype.isEditable = function () {
+ if (this.isTrash() || this.isShare()) {
+ return false;
}
- else if (this.isSearch()) {
- return this.ref.name;
+
+ if (!this.isWithinGroup()) {
+ return true;
}
- else if (this.isShare()) {
- return this.ref.name;
+ var libraryID = this.ref.libraryID;
+ if (this.isGroup()) {
+ return this.ref.editable;
}
- else if (this.isTrash()) {
- return Zotero.getString('pane.collections.trash');
+ if (this.isCollection()) {
+ var type = Zotero.Libraries.getType(libraryID);
+ if (type == 'group') {
+ var groupID = Zotero.Groups.getGroupIDFromLibraryID(libraryID);
+ var group = Zotero.Groups.get(groupID);
+ return group.editable;
+ }
+ else {
+ throw ("Unknown library type '" + type + "' in Zotero.ItemGroup.isEditable()");
+ }
}
- else {
- return "";
+}
+
+
+Zotero.ItemGroup.prototype.getName = function()
+{
+ switch (this.type) {
+ case 'collection':
+ return this.ref.name;
+
+ case 'library':
+ return Zotero.getString('pane.collections.library');
+
+ case 'search':
+ return this.ref.name;
+
+ case 'share':
+ return this.ref.name;
+
+ case 'trash':
+ return Zotero.getString('pane.collections.trash');
+
+ case 'group':
+ return this.ref.name;
+
+ case 'header':
+ return this.ref.label;
+
+ default:
+ return "";
}
}
Zotero.ItemGroup.prototype.getChildItems = function()
{
- // Fake results if this is a shared library
- if (this.isShare()) {
- return this.ref.getAll();
+ switch (this.type) {
+ // Fake results if this is a shared library
+ case 'share':
+ return this.ref.getAll();
+
+ case 'header':
+ return [];
}
var s = this.getSearchObject();
+
+ // FIXME: Hack to exclude group libraries for now
+ if (this.isSearch()) {
+ var groups = Zotero.Groups.getAll();
+ for each(var group in groups) {
+ s.addCondition('libraryID', 'isNot', group.libraryID);
+ }
+ }
+
try {
var ids;
if (this.showDuplicates) {
@@ -1125,8 +1567,14 @@ Zotero.ItemGroup.prototype.getSearchObject = function() {
var includeScopeChildren = false;
// Create/load the inner search
- var s = new Zotero.Search(this.isSearch() ? this.ref.id : null);
+ var s = new Zotero.Search();
if (this.isLibrary()) {
+ s.addCondition('libraryID', 'is', null);
+ s.addCondition('noChildren', 'true');
+ includeScopeChildren = true;
+ }
+ else if (this.isGroup()) {
+ s.addCondition('libraryID', 'is', this.ref.libraryID);
s.addCondition('noChildren', 'true');
includeScopeChildren = true;
}
@@ -1141,7 +1589,10 @@ Zotero.ItemGroup.prototype.getSearchObject = function() {
else if (this.isTrash()) {
s.addCondition('deleted', 'true');
}
- else if (!this.isSearch()) {
+ else if (this.isSearch()) {
+ s.id = this.ref.id;
+ }
+ else {
throw ('Invalid search mode in Zotero.ItemGroup.getSearchObject()');
}
@@ -1172,10 +1623,15 @@ Zotero.ItemGroup.prototype.getSearchObject = function() {
* Returns all the tags used by items in the current view
*/
Zotero.ItemGroup.prototype.getChildTags = function() {
- // TODO: implement?
- if (this.isShare()) {
- return false;
+ switch (this.type) {
+ // TODO: implement?
+ case 'share':
+ return false;
+
+ case 'header':
+ return false;
}
+
var s = this.getSearchObject();
return Zotero.Tags.getAllWithinSearch(s);
@@ -1196,8 +1652,10 @@ Zotero.ItemGroup.prototype.setTags = function(tags)
* Returns TRUE if saved search, quicksearch or tag filter
*/
Zotero.ItemGroup.prototype.isSearchMode = function() {
- if (this.isSearch() || this.isTrash()) {
- return true;
+ switch (this.type) {
+ case 'search':
+ case 'trash':
+ return true;
}
// Quicksearch
diff --git a/chrome/content/zotero/xpcom/data/collection.js b/chrome/content/zotero/xpcom/data/collection.js
@@ -21,18 +21,23 @@
*/
-Zotero.Collection = function(collectionID) {
- this._collectionID = collectionID ? collectionID : null;
+Zotero.Collection = function() {
+ if (arguments[0]) {
+ throw ("Zotero.Collection constructor doesn't take any parameters");
+ }
+
this._init();
}
Zotero.Collection.prototype._init = function () {
// Public members for access by public methods -- do not access directly
+ this._id = null;
+ this._libraryID = null
+ this._key = null;
this._name = null;
- this._parent = null;
+ this._parent = false;
this._dateAdded = null;
this._dateModified = null;
- this._key = null;
this._loaded = false;
this._changed = false;
@@ -50,44 +55,66 @@ Zotero.Collection.prototype._init = function () {
}
-Zotero.Collection.prototype.__defineGetter__('id', function () { return this._collectionID; });
-
-Zotero.Collection.prototype.__defineSetter__('collectionID', function (val) { this._set('collectionID', val); });
+Zotero.Collection.prototype.__defineGetter__('objectType', function () { return 'collection'; });
+Zotero.Collection.prototype.__defineGetter__('id', function () { return this._get('id'); });
+Zotero.Collection.prototype.__defineSetter__('id', function (val) { this._set('id', val); });
+Zotero.Collection.prototype.__defineGetter__('libraryID', function () { return this._get('libraryID'); });
+Zotero.Collection.prototype.__defineSetter__('libraryID', function (val) { return this._set('libraryID', val); });
+Zotero.Collection.prototype.__defineGetter__('key', function () { return this._get('key'); });
+Zotero.Collection.prototype.__defineSetter__('key', function (val) { this._set('key', val) });
Zotero.Collection.prototype.__defineGetter__('name', function () { return this._get('name'); });
Zotero.Collection.prototype.__defineSetter__('name', function (val) { this._set('name', val); });
Zotero.Collection.prototype.__defineGetter__('parent', function () { return this._get('parent'); });
Zotero.Collection.prototype.__defineSetter__('parent', function (val) { this._set('parent', val); });
+Zotero.Collection.prototype.__defineGetter__('parentKey', function () { return this._get('parentKey'); });
+Zotero.Collection.prototype.__defineSetter__('parentKey', function (val) { this._set('parentKey', val); });
Zotero.Collection.prototype.__defineGetter__('dateAdded', function () { return this._get('dateAdded'); });
Zotero.Collection.prototype.__defineSetter__('dateAdded', function (val) { this._set('dateAdded', val); });
Zotero.Collection.prototype.__defineGetter__('dateModified', function () { return this._get('dateModified'); });
Zotero.Collection.prototype.__defineSetter__('dateModified', function (val) { this._set('dateModified', val); });
-Zotero.Collection.prototype.__defineGetter__('key', function () { return this._get('key'); });
-Zotero.Collection.prototype.__defineSetter__('key', function (val) { this._set('key', val); });
//Zotero.Collection.prototype.__defineSetter__('childCollections', function (arr) { this._setChildCollections(arr); });
Zotero.Collection.prototype.__defineSetter__('childItems', function (arr) { this._setChildItems(arr); });
-
Zotero.Collection.prototype._get = function (field) {
- if (this.id && !this._loaded) {
+ if ((this._id || this._key) && !this._loaded) {
this.load();
}
+
+ switch (field) {
+ case 'parent':
+ return this._getParent();
+
+ case 'parentKey':
+ return this._getParentKey();
+ }
+
return this['_' + field];
}
Zotero.Collection.prototype._set = function (field, val) {
- if (field == 'name') {
- val = Zotero.Utilities.prototype.trim(val);
- }
-
switch (field) {
- case 'id': // set using constructor
- //case 'collectionID': // set using constructor
- throw ("Invalid field '" + field + "' in Zotero.Collection.set()");
+ case 'id':
+ case 'libraryID':
+ case 'key':
+ if (val == this['_' + field]) {
+ return;
+ }
+
+ if (this._loaded) {
+ throw ("Cannot set " + field + " after object is already loaded in Zotero.Collection._set()");
+ }
+ //this._checkValue(field, val);
+ this['_' + field] = val;
+ return;
+
+ case 'name':
+ val = Zotero.Utilities.prototype.trim(val);
+ break;
}
- if (this.id) {
+ if (this.id || this.key) {
if (!this._loaded) {
this.load();
}
@@ -96,6 +123,16 @@ Zotero.Collection.prototype._set = function (field, val) {
this._loaded = true;
}
+ switch (field) {
+ case 'parent':
+ this._setParent(val);
+ return;
+
+ case 'parentKey':
+ this._setParentKey(val);
+ return;
+ }
+
if (this['_' + field] != val) {
this._prepFieldChange(field);
@@ -126,16 +163,35 @@ Zotero.Collection.prototype.getParent = function() {
* Build collection from database
*/
Zotero.Collection.prototype.load = function() {
+ var id = this._id;
+ var key = this._key;
+ var libraryID = this._libraryID;
+ //var desc = id ? id : libraryID + "/" + key;
+
// Should be same as query in Zotero.Collections, just with collectionID
var sql = "SELECT C.*, "
+ "(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=?";
- var data = Zotero.DB.rowQuery(sql, this.id);
+ + "FROM collections C WHERE ";
+ if (id) {
+ sql += "collectionID=?";
+ var params = id;
+ }
+ else {
+ sql += "key=?";
+ var params = [key];
+ if (libraryID) {
+ sql += " AND libraryID=?";
+ params.push(libraryID);
+ }
+ else {
+ sql += " AND libraryID IS NULL";
+ }
+ }
+ var data = Zotero.DB.rowQuery(sql, params);
- this._init();
this._loaded = true;
if (!data) {
@@ -154,13 +210,16 @@ Zotero.Collection.prototype.loadFromRow = function(row) {
this._changed = false;
this._previousData = false;
- this._collectionID = row.collectionID;
+ this._id = row.collectionID;
+ this._libraryID = row.libraryID;
+ this._key = row.key;
this._name = row.collectionName;
this._parent = row.parentCollectionID;
this._dateAdded = row.dateAdded;
this._dateModified = row.dateModified;
- this._key = row.key;
+ this._childCollectionsLoaded = false;
this._hasChildCollections = !!row.hasChildCollections;
+ this._childItemsLoaded = false;
this._hasChildItems = !!row.hasChildItems;
this._loadChildItems();
}
@@ -288,6 +347,8 @@ Zotero.Collection.prototype.unlockDateModified = function () {
Zotero.Collection.prototype.save = function () {
+ Zotero.Collections.editCheck(this);
+
if (!this.name) {
throw ('Collection name is empty in Zotero.Collection.save()');
}
@@ -297,54 +358,8 @@ Zotero.Collection.prototype.save = function () {
return false;
}
- if (this._changed.parent && this.parent) {
- if (!Zotero.Collections.get(this.parent)) {
- throw ('Cannot set parent of collection ' + this.id
- + ' to invalid parent ' + this.parent);
- }
-
- if (this.parent == this.id) {
- throw ('Cannot move collection into itself!');
- }
-
- if (this.id && this.hasDescendent('collection', this.parent)) {
- throw ('Cannot move collection into one of its own descendents!', 2);
- }
- }
-
-
Zotero.DB.beginTransaction();
- // ID change
- if (this._changed['collectionID']) {
- var oldID = this._previousData.primary.collectionID;
- var params = [this.id, oldID];
-
- Zotero.debug("Changing collectionID " + oldID + " to " + this.id);
-
- var row = Zotero.DB.rowQuery("SELECT * FROM collections WHERE collectionID=?", oldID);
- // Add a new row so we can update the old rows despite FK checks
- // Use temp key due to UNIQUE constraint on key column
- Zotero.DB.query("INSERT INTO collections VALUES (?, ?, ?, ?, ?, ?)",
- [this.id, row.collectionName, row.parentCollectionID,
- row.dateAdded, row.dateModified, 'TEMPKEY']);
-
- Zotero.DB.query("UPDATE collectionItems SET collectionID=? WHERE collectionID=?", params);
- Zotero.DB.query("UPDATE collections SET parentCollectionID=? WHERE parentCollectionID=?", params);
-
- Zotero.DB.query("DELETE FROM collections WHERE collectionID=?", oldID);
- Zotero.DB.query("UPDATE collections SET key=? WHERE collectionID=?", [row.key, this.id]);
-
- Zotero.Notifier.trigger('id-change', 'collection', oldID + '-' + this.id);
-
- // Update child collections that have cached the previous id
- var sql = "SELECT collectionID FROM collections WHERE parentCollectionID=?";
- var children = Zotero.DB.columnQuery(sql, this.id);
- if (children) {
- Zotero.Collections.refreshParents(children);
- }
- }
-
var isNew = !this.id || !this.exists();
try {
@@ -356,20 +371,55 @@ Zotero.Collection.prototype.save = function () {
var key = this.key ? this.key : this._generateKey();
+ // Verify parent
+ if (this._parent) {
+ if (typeof this._parent == 'number') {
+ var newParent = Zotero.Collections.get(this._parent);
+ }
+ else {
+ var newParent = Zotero.Collections.getByLibraryAndKey(this.libraryID, this._parent);
+ }
+
+ if (!newParent) {
+ throw("Cannot set parent to invalid collection " + this._parent + " in Zotero.Collection.save()");
+ }
+
+ if (newParent.id == this.id) {
+ throw ('Cannot move collection into itself!');
+ }
+
+ if (this.id && this.hasDescendent('collection', newParent.id)) {
+ throw ('Cannot move collection into one of its own descendents!', 2);
+ }
+
+ var parent = newParent.id;
+ }
+ else {
+ var parent = null;
+ }
+
var columns = [
- 'collectionID', 'collectionName', 'parentCollectionID',
- 'dateAdded', 'dateModified', 'key'
+ 'collectionID',
+ 'collectionName',
+ 'parentCollectionID',
+ 'dateAdded',
+ 'dateModified',
+ 'clientDateModified',
+ 'libraryID',
+ 'key'
];
- var placeholders = ['?', '?', '?', '?', '?', '?'];
+ var placeholders = ['?', '?', '?', '?', '?', '?', '?', '?'];
var sqlValues = [
collectionID ? { int: collectionID } : null,
{ string: this.name },
- this.parent ? { int: this.parent } : null,
+ parent ? parent : null,
// If date added isn't set, use current timestamp
this.dateAdded ? this.dateAdded : Zotero.DB.transactionDateTime,
// If date modified hasn't changed, use current timestamp
this._changed.dateModified ?
this.dateModified : Zotero.DB.transactionDateTime,
+ Zotero.DB.transactionDateTime,
+ this.libraryID ? this.libraryID : null,
key
];
@@ -380,20 +430,19 @@ Zotero.Collection.prototype.save = function () {
collectionID = insertID;
}
-
if (this._changed.parent) {
var parentIDs = [];
- if (this._previousData.parent) {
+ if (this.id && this._previousData.parent) {
parentIDs.push(this._previousData.parent);
}
if (this.parent) {
parentIDs.push(this.parent);
}
-
- Zotero.Notifier.trigger('move', 'collection', this.id);
+ if (this.id) {
+ Zotero.Notifier.trigger('move', 'collection', this.id);
+ }
}
-
/*
// Subcollections
if (this._changed.childCollections) {
@@ -502,6 +551,12 @@ Zotero.Collection.prototype.save = function () {
insertStatement.execute();
}
catch (e) {
+ Zotero.debug('=======');
+ Zotero.debug(collectionID);
+ Zotero.debug(itemID);
+ Zotero.debug(orderIndex);
+ Zotero.debug(Zotero.DB.query("SELECT * FROM collections"));
+ Zotero.debug(Zotero.DB.query("SELECT * FROM collectionItems"));
throw (e + ' [ERROR: ' + Zotero.DB.getLastErrorString() + ']');
}
}
@@ -510,6 +565,11 @@ Zotero.Collection.prototype.save = function () {
//Zotero.Notifier.trigger('add', 'collection-item', this.id + '-' + itemID);
}
+ if (this._changed.libraryID) {
+ var groupID = Zotero.Libraries.getGroupIDFromLibraryID(this.libraryID);
+ var group = Zotero.Groups.get(groupID);
+ group.clearCollectionsCache();
+ }
Zotero.DB.commitTransaction();
}
catch (e) {
@@ -519,7 +579,7 @@ Zotero.Collection.prototype.save = function () {
// If successful, set values in object
if (!this.id) {
- this._collectionID = collectionID;
+ this._id = collectionID;
}
if (!this.key) {
@@ -537,7 +597,7 @@ Zotero.Collection.prototype.save = function () {
// Invalidate cached child collections
if (parentIDs) {
- Zotero.Collections.refreshChildCollections(parentIDs)
+ Zotero.Collections.refreshChildCollections(parentIDs);
}
return this.id;
@@ -569,8 +629,8 @@ Zotero.Collection.prototype.addItem = function(itemID) {
sql = "INSERT OR IGNORE INTO collectionItems VALUES (?,?,?)";
Zotero.DB.query(sql, [this.id, itemID, nextOrderIndex]);
- sql = "UPDATE collections SET dateModified=? WHERE collectionID=?";
- Zotero.DB.query(sql, [Zotero.DB.transactionDateTime, this.id]);
+ sql = "UPDATE collections SET dateModified=?, clientDateModified=? WHERE collectionID=?";
+ Zotero.DB.query(sql, [Zotero.DB.transactionDateTime, Zotero.DB.transactionDateTime, this.id]);
Zotero.DB.commitTransaction();
@@ -618,8 +678,8 @@ Zotero.Collection.prototype.removeItem = function(itemID) {
Zotero.DB.query(sql, [this.id, itemID]);
if (!this._dateModifiedLocked) {
- sql = "UPDATE collections SET dateModified=? WHERE collectionID=?";
- Zotero.DB.query(sql, [Zotero.DB.transactionDateTime, this.id])
+ sql = "UPDATE collections SET dateModified=?, clientDateModified=? WHERE collectionID=?";
+ Zotero.DB.query(sql, [Zotero.DB.transactionDateTime, Zotero.DB.transactionDateTime, this.id])
}
Zotero.DB.commitTransaction();
@@ -812,9 +872,10 @@ Zotero.Collection.prototype.serialize = function(nested) {
var obj = {
primary: {
collectionID: this.id,
+ libraryID: this.libraryID,
+ key: this.key,
dateAdded: this.dateAdded,
- dateModified: this.dateModified,
- key: this.key
+ dateModified: this.dateModified
},
fields: {
name: this.name,
@@ -835,7 +896,7 @@ Zotero.Collection.prototype.serialize = function(nested) {
* @param bool nested Return multidimensional array with 'children'
* nodes instead of flat array
* @param string type 'item', 'collection', or FALSE for both
- * @return {Object[]} Array of objects with 'id',
+ * @return {Object[]} Array of objects with 'id', 'key',
* 'type' ('item' or 'collection'), 'parent',
* and, if collection, 'name' and the nesting 'level'
*/
@@ -853,10 +914,10 @@ Zotero.Collection.prototype.getChildren = function(recursive, nested, type, leve
// 0 == collection
// 1 == item
var children = Zotero.DB.query('SELECT collectionID AS id, '
- + "0 AS type, collectionName AS collectionName "
+ + "0 AS type, collectionName AS collectionName, key "
+ 'FROM collections WHERE parentCollectionID=?1'
- + ' UNION SELECT itemID AS id, 1 AS type, NULL AS collectionName '
- + 'FROM collectionItems WHERE collectionID=?1', this.id);
+ + ' UNION SELECT itemID AS id, 1 AS type, NULL AS collectionName, key '
+ + 'FROM collectionItems JOIN items USING (itemID) WHERE collectionID=?1', this.id);
if (type) {
switch (type) {
@@ -879,6 +940,7 @@ Zotero.Collection.prototype.getChildren = function(recursive, nested, type, leve
toReturn.push({
id: children[i].id,
name: children[i].collectionName,
+ key: children[i].key,
type: 'collection',
level: level,
parent: this.id
@@ -905,6 +967,7 @@ Zotero.Collection.prototype.getChildren = function(recursive, nested, type, leve
if (!type || type=='item') {
toReturn.push({
id: children[i].id,
+ key: children[i].key,
type: 'item',
parent: this.id
});
@@ -939,6 +1002,139 @@ Zotero.Collection.prototype._prepFieldChange = function (field) {
}
+/**
+ * Get the collectionID of the parent collection
+ * @return {Integer}
+ */
+Zotero.Collection.prototype._getParent = function() {
+ if (this._parent !== false) {
+ if (!this._parent) {
+ return null;
+ }
+ if (typeof this._parent == 'number') {
+ return this._parent;
+ }
+ var parentCollection = Zotero.Collections.getByLibraryAndKey(this.libraryID, this._parent);
+ if (!parentCollection) {
+ throw ("Parent collection for keyed parent doesn't exist in Zotero.Collection._getParent()");
+ }
+ // Replace stored key with id
+ this._parent = parentCollection.id;
+ return parentCollection.id;
+ }
+
+ if (!this.id) {
+ return false;
+ }
+
+ var sql = "SELECT parentCollectionID FROM collections WHERE collectionID=?";
+ var parentCollectionID = Zotero.DB.valueQuery(sql, this.id);
+ if (!parentCollectionID) {
+ parentCollectionID = null;
+ }
+ this._parent = parentCollectionID;
+ return parentCollectionID;
+}
+
+
+/**
+ * Get the key of the parent collection
+ * @return {String}
+ */
+Zotero.Collection.prototype._getParentKey = function() {
+ if (this._parent !== false) {
+ if (!this._parent) {
+ return null;
+ }
+ if (typeof this._parent == 'string') {
+ return this._parent;
+ }
+ var parentCollection = Zotero.Collections.get(this._parent);
+ return parentCollection.key;
+ }
+
+ if (!this.id) {
+ return false;
+ }
+
+ var sql = "SELECT B.key FROM collections A JOIN collections B "
+ + "ON (A.parentCollectionID=B.collectionID) WHERE A.collectionID=?";
+ var key = Zotero.DB.valueQuery(sql, this.id);
+ if (!key) {
+ key = null;
+ }
+ this._parent = key;
+ return key;
+}
+
+
+Zotero.Collection.prototype._setParent = function(parentCollectionID) {
+ if (this.id || this.key) {
+ if (!this.loaded) {
+ this.load(true);
+ }
+ }
+ else {
+ this.loaded = true;
+ }
+
+ var oldParentCollectionID = this._getParent();
+ if (oldParentCollectionID == parentCollectionID) {
+ Zotero.debug("Parent collection has not changed for collection " + this.id);
+ return false;
+ }
+
+ if (this.id && this.exists() && !this._previousData) {
+ this._previousData = this.serialize();
+ }
+
+ this._parent = parentCollectionID ? parseInt(parentCollectionID) : null;
+ if (!this._changed) {
+ this._changed = {};
+ }
+ this._changed.parent = true;
+
+ return true;
+}
+
+
+Zotero.Collection.prototype._setParentKey = function(parentCollectionKey) {
+ if (this.id || this.key) {
+ if (!this.loaded) {
+ this.load(true);
+ }
+ }
+ else {
+ this.loaded = true;
+ }
+
+ var oldParentCollectionID = this._getParent();
+ if (oldParentCollectionID) {
+ var parentCollection = Zotero.Collections.get(oldParentCollectionID)
+ var oldParentCollectionKey = parentCollection.key;
+ }
+ else {
+ var oldParentCollectionKey = null;
+ }
+ if (oldParentCollectionKey == parentCollectionKey) {
+ Zotero.debug("Parent collection has not changed in Zotero.Collection._setParentKey()");
+ return false;
+ }
+
+ if (this.id && this.exists() && !this._previousData) {
+ this._previousData = this.serialize();
+ }
+
+ this._parent = parentCollectionKey ? parentCollectionKey : null;
+ if (!this._changed) {
+ this._changed = {};
+ }
+ this._changed.parent = true;
+
+ return true;
+}
+
+
/*
Zotero.Collection.prototype._setChildCollections = function (collectionIDs) {
this._setChildren('collection', collectionIDs);
@@ -1053,7 +1249,13 @@ Zotero.Collection.prototype._loadChildCollections = function () {
}
Zotero.Collection.prototype._loadChildItems = function() {
- var sql = "SELECT itemID FROM collectionItems WHERE collectionID=? ";
+ if (!this.id) {
+ //throw ("Collection id not set in Zotero.Collection._loadChildItems()");
+ this._childItemsLoaded = true;
+ return;
+ }
+
+ var sql = "SELECT itemID FROM collectionItems WHERE collectionID=? "
// DEBUG: Fix for child items created via context menu on parent within
// a collection being added to the current collection
+ "AND itemID NOT IN "
@@ -1075,22 +1277,6 @@ Zotero.Collection.prototype._loadChildItems = function() {
/**
- * Note: This is called by Zotero.Collections.refreshParent()
- *
- * @private
- */
-Zotero.Collection.prototype._refreshParent = function () {
- if (!this.id) {
- throw ("Cannot call Zotero.Collection._refreshParent() on unsaved collection");
- }
-
- var sql = "SELECT parentCollectionID FROM collections "
- + "WHERE collectionID=?";
- this._parent = Zotero.DB.valueQuery(sql, this.id);
-}
-
-
-/**
* Invalid child collection cache
*
* Note: This is called by Zotero.Collections.refreshChildCollections()
diff --git a/chrome/content/zotero/xpcom/data/collections.js b/chrome/content/zotero/xpcom/data/collections.js
@@ -81,23 +81,6 @@ Zotero.Collections = new function() {
/**
- * Refresh cached parents in specified collections, skipping
- * any that aren't loaded
- *
- * @param {Integer|Integer[]} ids One or more itemIDs
- */
- this.refreshParents = function (ids) {
- ids = Zotero.flattenArguments(ids);
-
- for each(var id in ids) {
- if (this._objectCache[id]) {
- this._objectCache[id]._refreshParent();
- }
- }
- }
-
-
- /**
* Invalidate child collection cache in specified collections, skipping
* any that aren't loaded
*
diff --git a/chrome/content/zotero/xpcom/data/creator.js b/chrome/content/zotero/xpcom/data/creator.js
@@ -21,18 +21,23 @@
*/
-Zotero.Creator = function (creatorID) {
- this._creatorID = creatorID ? creatorID : null;
+Zotero.Creator = function () {
+ if (arguments[0]) {
+ throw ("Zotero.Creator constructor doesn't take any parameters");
+ }
+
this._init();
}
Zotero.Creator.prototype._init = function () {
+ this._id = null;
+ this._libraryID = null
+ this._key = null;
this._firstName = null;
this._lastName = null;
this._fieldMode = null;
this._birthYear = null;
- this._key = null;
this._dateAdded = null;
this._dateModified = null;
@@ -43,10 +48,14 @@ Zotero.Creator.prototype._init = function () {
}
-Zotero.Creator.prototype.__defineGetter__('id', function () { return this._creatorID; });
+Zotero.Creator.prototype.__defineGetter__('objectType', function () { return 'creator'; });
+Zotero.Creator.prototype.__defineGetter__('id', function () { return this._get('id'); });
+Zotero.Creator.prototype.__defineSetter__('id', function (val) { this._set('id', val); });
+Zotero.Creator.prototype.__defineGetter__('libraryID', function () { return this._get('libraryID'); });
+Zotero.Creator.prototype.__defineSetter__('libraryID', function (val) { return this._set('libraryID', val); });
+Zotero.Creator.prototype.__defineGetter__('key', function () { return this._get('key'); });
+Zotero.Creator.prototype.__defineSetter__('key', function (val) { this._set('key', val) });
Zotero.Creator.prototype.__defineGetter__('creatorDataID', function () { return this._get('creatorDataID'); });
-
-Zotero.Creator.prototype.__defineSetter__('creatorID', function (val) { this._set('creatorID', val); });
Zotero.Creator.prototype.__defineGetter__('firstName', function () { return this._get('firstName'); });
Zotero.Creator.prototype.__defineSetter__('firstName', function (val) { this._set('firstName', val); });
Zotero.Creator.prototype.__defineGetter__('lastName', function () { return this._get('lastName'); });
@@ -59,16 +68,13 @@ Zotero.Creator.prototype.__defineGetter__('dateAdded', function () { return this
Zotero.Creator.prototype.__defineSetter__('dateAdded', function (val) { this._set('dateAdded', val); });
Zotero.Creator.prototype.__defineGetter__('dateModified', function () { return this._get('dateModified'); });
Zotero.Creator.prototype.__defineSetter__('dateModified', function (val) { this._set('dateModified', val); });
-Zotero.Creator.prototype.__defineGetter__('key', function () { return this._get('key'); });
-Zotero.Creator.prototype.__defineSetter__('key', function (val) { this._set('key', val); });
// Block properties that can't be set this way
-Zotero.Creator.prototype.__defineSetter__('id', function () { this._set('id', val); });
-Zotero.Creator.prototype.__defineSetter__('creatorDataID', function () { this._set('creatorDataID', val); });
+//Zotero.Creator.prototype.__defineSetter__('creatorDataID', function () { this._set('creatorDataID', val); });
Zotero.Creator.prototype._get = function (field) {
- if (this.id && !this._loaded) {
+ if ((this._id || this._key) && !this._loaded) {
this.load(true);
}
return this['_' + field];
@@ -77,26 +83,40 @@ Zotero.Creator.prototype._get = function (field) {
Zotero.Creator.prototype._set = function (field, val) {
switch (field) {
+ case 'id':
+ case 'libraryID':
+ case 'key':
+ if (val == this['_' + field]) {
+ return;
+ }
+
+ if (this._loaded) {
+ throw ("Cannot set " + field + " after object is already loaded in Zotero.Creator._set()");
+ }
+ this._checkValue(field, val);
+ this['_' + field] = val;
+ return;
+
case 'firstName':
case 'lastName':
case 'shortName':
if (val) {
- val = value = Zotero.Utilities.prototype.trim(val);
+ val = Zotero.Utilities.prototype.trim(val);
}
else {
- val = value = '';
+ val = '';
}
break;
- }
-
- switch (field) {
- case 'id': // set using constructor
- //case 'creatorID': // set using constructor
+
+ case 'fieldMode':
+ val = val ? parseInt(val) : 0;
+ break;
+
case 'creatorDataID':
throw ("Invalid field '" + field + "' in Zotero.Creator.set()");
}
- if (this.id) {
+ if (this.id || this.key) {
if (!this._loaded) {
this.load(true);
}
@@ -121,10 +141,11 @@ Zotero.Creator.prototype._set = function (field, val) {
}
-Zotero.Creator.prototype.setFields = function(fields) {
- for (var field in fields) {
- this[field] = fields[field];
- }
+Zotero.Creator.prototype.setFields = function (fields) {
+ this.firstName = fields.firstName;
+ this.lastName = fields.lastName;
+ this.fieldMode = fields.fieldMode;
+ this.birthYear = fields.birthYear;
}
@@ -149,6 +170,10 @@ Zotero.Creator.prototype.hasChanged = function () {
Zotero.Creator.prototype.save = function () {
+ Zotero.Creators.editCheck(this);
+
+ Zotero.debug("Saving creator " + this.id);
+
if (!this.firstName && !this.lastName) {
throw ('First and last name are empty in Zotero.Creator.save()');
}
@@ -164,32 +189,6 @@ Zotero.Creator.prototype.save = function () {
Zotero.DB.beginTransaction();
- // ID change
- if (this._changed['creatorID']) {
- var oldID = this._previousData.primary.creatorID;
- var params = [this.id, oldID];
-
- Zotero.debug("Changing creatorID " + oldID + " to " + this.id);
-
- var row = Zotero.DB.rowQuery("SELECT * FROM creators WHERE creatorID=?", oldID);
- // Add a new row so we can update the old rows despite FK checks
- // Use temp key due to UNIQUE constraint on key column
- Zotero.DB.query("INSERT INTO creators VALUES (?, ?, ?, ?, ?)",
- [this.id, row.creatorDataID, row.dateAdded, row.dateModified, 'TEMPKEY']);
-
- Zotero.DB.query("UPDATE itemCreators SET creatorID=? WHERE creatorID=?", params);
-
- Zotero.DB.query("DELETE FROM creators WHERE creatorID=?", oldID);
- Zotero.DB.query("UPDATE creators SET key=? WHERE creatorID=?", [row.key, this.id]);
-
- Zotero.Notifier.trigger('id-change', 'creator', oldID + '-' + this.id);
-
- // Do this here because otherwise updateLinkedItems() below would
- // load a duplicate copy in the new position
- Zotero.Creators.reload(this.id);
- // update caches
- }
-
var isNew = !this.id || !this.exists();
try {
@@ -197,8 +196,6 @@ Zotero.Creator.prototype.save = function () {
var creatorID = this.id ? this.id : Zotero.ID.get('creators');
- Zotero.debug("Saving creator " + this.id);
-
var key = this.key ? this.key : this._generateKey();
// If this was the only creator with the previous data,
@@ -229,8 +226,16 @@ Zotero.Creator.prototype.save = function () {
var creatorDataID = Zotero.Creators.getDataID(this, true);
}
- var columns = ['creatorID', 'creatorDataID', 'dateAdded', 'dateModified', 'key'];
- var placeholders = ['?', '?', '?', '?', '?'];
+ var columns = [
+ 'creatorID',
+ 'creatorDataID',
+ 'dateAdded',
+ 'dateModified',
+ 'clientDateModified',
+ 'libraryID',
+ 'key'
+ ];
+ var placeholders = ['?', '?', '?', '?', '?', '?', '?'];
var sqlValues = [
creatorID ? { int: creatorID } : null,
{ int: creatorDataID },
@@ -239,6 +244,8 @@ Zotero.Creator.prototype.save = function () {
// If date modified hasn't changed, use current timestamp
this._changed.dateModified ?
this.dateModified : Zotero.DB.transactionDateTime,
+ Zotero.DB.transactionDateTime,
+ this.libraryID ? this.libraryID : null,
key
];
@@ -267,7 +274,7 @@ Zotero.Creator.prototype.save = function () {
// If successful, set values in object
if (!this.id) {
- this._creatorID = creatorID;
+ this._id = creatorID;
}
if (!this.key) {
this._key = key;
@@ -320,9 +327,9 @@ Zotero.Creator.prototype.updateLinkedItems = function () {
}
}
- sql = "UPDATE items SET dateModified=? WHERE itemID IN "
+ sql = "UPDATE items SET dateModified=?, clientDateModified=? WHERE itemID IN "
+ "(SELECT itemID FROM itemCreators WHERE creatorID=?)";
- Zotero.DB.query(sql, [Zotero.DB.transactionDateTime, this.id]);
+ Zotero.DB.query(sql, [Zotero.DB.transactionDateTime, Zotero.DB.transactionDateTime, this.id]);
Zotero.Items.reload(changedItemIDs);
@@ -346,9 +353,10 @@ Zotero.Creator.prototype.serialize = function () {
obj.primary = {};
obj.primary.creatorID = this.id;
+ obj.primary.libraryID = this.libraryID;
+ obj.primary.key = this.key;
obj.primary.dateAdded = this.dateAdded;
obj.primary.dateModified = this.dateModified;
- obj.primary.key = this.key;
obj.fields = {};
if (this.fieldMode == 1) {
@@ -419,22 +427,41 @@ Zotero.Creator.prototype.erase = function () {
Zotero.Creator.prototype.load = function (allowFail) {
- Zotero.debug("Loading data for creator " + this.id + " in Zotero.Creator.load()");
+ var id = this._id;
+ var key = this._key;
+ var libraryID = this._libraryID;
+ var desc = id ? id : libraryID + "/" + key;
- if (!this.id) {
- throw ("creatorID not set in Zotero.Creator.load()");
+ Zotero.debug("Loading data for creator " + desc + " in Zotero.Creator.load()");
+
+ if (!id && !key) {
+ throw ("ID or key not set in Zotero.Creator.load()");
}
- var sql = "SELECT C.*, CD.* FROM creators C NATURAL JOIN creatorData CD "
- + "WHERE creatorID=?";
- var row = Zotero.DB.rowQuery(sql, this.id);
+ var sql = "SELECT C.*, CD.* FROM creators C NATURAL JOIN creatorData CD WHERE ";
+ if (id) {
+ sql += "creatorID=?";
+ var params = id;
+ }
+ else {
+ sql += "key=?";
+ var params = [key];
+ if (libraryID) {
+ sql += " AND libraryID=?";
+ params.push(libraryID);
+ }
+ else {
+ sql += " AND libraryID IS NULL";
+ }
+ }
+ var row = Zotero.DB.rowQuery(sql, params);
if (!row) {
if (allowFail) {
this._loaded = true;
return false;
}
- throw ("Creator " + this.id + " not found in Zotero.Item.load()");
+ throw ("Creator " + desc + " not found in Zotero.Item.load()");
}
this.loadFromRow(row);
@@ -444,12 +471,22 @@ Zotero.Creator.prototype.load = function (allowFail) {
Zotero.Creator.prototype.loadFromRow = function (row) {
this._init();
-
for (var col in row) {
//Zotero.debug("Setting field '" + col + "' to '" + row[col] + "' for creator " + this.id);
+ switch (col) {
+ case 'clientDateModified':
+ continue;
+
+ case 'creatorID':
+ this._id = row[col];
+ continue;
+
+ case 'libraryID':
+ this['_' + col] = row[col] ? row[col] : null;
+ continue;
+ }
this['_' + col] = row[col] ? row[col] : '';
}
-
this._loaded = true;
}
@@ -462,6 +499,18 @@ Zotero.Creator.prototype._checkValue = function (field, value) {
// Data validation
switch (field) {
+ case 'id':
+ if (parseInt(value) != value) {
+ this._invalidValueError(field, value);
+ }
+ break;
+
+ case 'libraryID':
+ if (value && parseInt(value) != value) {
+ this._invalidValueError(field, value);
+ }
+ break;
+
case 'fieldMode':
if (value !== 0 && value !== 1) {
this._invalidValueError(field, value);
diff --git a/chrome/content/zotero/xpcom/data/creators.js b/chrome/content/zotero/xpcom/data/creators.js
@@ -42,6 +42,10 @@ Zotero.Creators = new function() {
* Returns a Zotero.Creator object for a given creatorID
*/
function get(creatorID) {
+ if (!creatorID) {
+ throw ("creatorID not provided in Zotero.Creators.get()");
+ }
+
if (this._objectCache[creatorID]) {
return this._objectCache[creatorID];
}
@@ -53,7 +57,9 @@ Zotero.Creators = new function() {
return false;
}
- this._objectCache[creatorID] = new Zotero.Creator(creatorID);
+ var creator = new Zotero.Creator;
+ creator.id = creatorID;
+ this._objectCache[creatorID] = creator;
return this._objectCache[creatorID];
}
@@ -114,15 +120,28 @@ Zotero.Creators = new function() {
}
- function getCreatorsWithData(creatorDataID) {
+ function getCreatorsWithData(creatorDataID, libraryID) {
var sql = "SELECT creatorID FROM creators WHERE creatorDataID=?";
- return Zotero.DB.columnQuery(sql, creatorDataID);
+ var params = [creatorDataID];
+ if (libraryID) {
+ sql += " AND libraryID=?";
+ params.push(libraryID);
+ }
+ else {
+ sql += " AND libraryID IS NULL";
+ }
+ return Zotero.DB.columnQuery(sql, params);
}
- function countCreatorsWithData(creatorDataID) {
+ function countCreatorsWithData(creatorDataID, libraryID) {
var sql = "SELECT COUNT(*) FROM creators WHERE creatorDataID=?";
- return Zotero.DB.valueQuery(sql, creatorDataID);
+ var params = [creatorDataID];
+ if (libraryID) {
+ sql += " AND libraryID=?";
+ params.push(libraryID);
+ }
+ return Zotero.DB.valueQuery(sql, params);
}
diff --git a/chrome/content/zotero/xpcom/data/dataObjects.js b/chrome/content/zotero/xpcom/data/dataObjects.js
@@ -14,20 +14,86 @@ Zotero.DataObjects = function (object, objectPlural, id, table) {
this._ZDO_id = (id ? id : object) + 'ID';
this._ZDO_table = table ? table : this._ZDO_objects;
+ // Certain object types don't have a libary and key and only use an id
+ switch (object) {
+ case 'relation':
+ this._ZDO_idOnly = true;
+ break;
+
+ default:
+ this._ZDO_idOnly = false;
+ }
+
this._objectCache = {};
this._reloadCache = true;
+ this.makeLibraryKeyHash = function (libraryID, key) {
+ var libraryID = libraryID ? libraryID : 0;
+ return libraryID + '_' + key;
+ }
+
+
+ this.getLibraryKeyHash = function (obj) {
+ return this.makeLibraryKeyHash(obj.libraryID, obj.key);
+ }
+
+
+ this.parseLibraryKeyHash = function (libraryKey) {
+ var [libraryID, key] = libraryKey.split('_');
+ libraryID = parseInt(libraryID);
+ return {
+ libraryID: libraryID ? libraryID : null,
+ key: key
+ };
+ }
+
+
/**
- * Retrieves an object by its secondary lookup key
+ * Retrieves an object of the current by its key
*
- * @param string key Secondary lookup key
- * @return object Zotero data object, or FALSE if not found
+ * @param {String} key
+ * @return {Zotero.DataObject} Zotero data object, or FALSE if not found
*/
this.getByKey = function (key) {
- var sql = "SELECT " + this._ZDO_id + " FROM " + this._ZDO_table
- + " WHERE key=?";
- var id = Zotero.DB.valueQuery(sql, key);
+ if (arguments.length > 1) {
+ throw ("getByKey() takes only one argument");
+ }
+
+ Components.utils.reportError("Zotero." + this._ZDO_Objects
+ + ".getByKey() is deprecated -- use getByLibraryAndKey()");
+
+ return this.getByLibraryAndKey(null, key);
+ }
+
+
+ /**
+ * Retrieves an object by its libraryID and key
+ *
+ * @param {Integer|NULL} libraryID
+ * @param {String} key
+ * @return {Zotero.DataObject} Zotero data object, or FALSE if not found
+ */
+ this.getByLibraryAndKey = function (libraryID, key) {
+ var sql = "SELECT ROWID FROM " + this._ZDO_table + " WHERE ";
+ var params = [];
+ if (this._ZDO_idOnly) {
+ sql += "ROWID=?";
+ params.push(key);
+ }
+ else {
+ sql += "libraryID";
+ if (libraryID) {
+ sql += "=? ";
+ params.push(libraryID);
+ }
+ else {
+ sql += " IS NULL ";
+ }
+ sql += "AND key=?";
+ params.push(key);
+ }
+ var id = Zotero.DB.valueQuery(sql, params);
if (!id) {
return false;
}
@@ -41,8 +107,7 @@ Zotero.DataObjects = function (object, objectPlural, id, table) {
+ "Zotero." + this._ZDO_Objects + ".getOlder()")
}
- var sql = "SELECT " + this._ZDO_id + " FROM " + this._ZDO_table
- + " WHERE dateModified<?";
+ var sql = "SELECT ROWID FROM " + this._ZDO_table + " WHERE clientDateModified<?";
return Zotero.DB.columnQuery(sql, Zotero.Date.dateToSQL(date, true));
}
@@ -53,9 +118,9 @@ Zotero.DataObjects = function (object, objectPlural, id, table) {
+ "Zotero." + this._ZDO_Objects + ".getNewer()")
}
- var sql = "SELECT " + this._ZDO_id + " FROM " + this._ZDO_table;
+ var sql = "SELECT ROWID FROM " + this._ZDO_table;
if (date) {
- sql += " WHERE dateModified>?";
+ sql += " WHERE clientDateModified>?";
return Zotero.DB.columnQuery(sql, Zotero.Date.dateToSQL(date, true));
}
return Zotero.DB.columnQuery(sql);
@@ -75,6 +140,7 @@ Zotero.DataObjects = function (object, objectPlural, id, table) {
var ids = Zotero.flattenArguments(arguments);
Zotero.debug('Reloading ' + this._ZDO_objects + ' ' + ids);
+ /*
// Reset cache keys to itemIDs stored in database using object keys
var sql = "SELECT " + this._ZDO_id + " AS id, key FROM " + this._ZDO_table
+ " WHERE " + this._ZDO_id + " IN ("
@@ -87,22 +153,32 @@ Zotero.DataObjects = function (object, objectPlural, id, table) {
}
var store = {};
+ Zotero.debug('==================');
for (var id in this._objectCache) {
+ Zotero.debug('id is ' + id);
var obj = this._objectCache[id];
+ //Zotero.debug(obj);
var dbID = keyIDs[obj.key];
+ Zotero.debug("DBID: " + dbID);
if (!dbID || id == dbID) {
+ Zotero.debug('continuing');
continue;
}
+ Zotero.debug('Assigning ' + dbID + ' to store');
store[dbID] = obj;
+ Zotero.debug('deleting ' + id);
delete this._objectCache[id];
}
+ Zotero.debug('------------------');
for (var id in store) {
+ Zotero.debug(id);
if (this._objectCache[id]) {
throw("Existing " + this._ZDO_object + " " + id
+ " exists in cache in Zotero.DataObjects.reload()");
}
this._objectCache[id] = store[id];
}
+ */
// If there's an internal reload hook, call it
if (this._reload) {
@@ -119,22 +195,16 @@ Zotero.DataObjects = function (object, objectPlural, id, table) {
this.reloadAll = function () {
Zotero.debug("Reloading all " + this._ZDO_objects);
- // Reset cache keys to itemIDs stored in database using object keys
- var sql = "SELECT " + this._ZDO_id + " AS id, key FROM " + this._ZDO_table;
- var rows = Zotero.DB.query(sql);
+ // Remove objects not stored in database
+ var sql = "SELECT ROWID FROM " + this._ZDO_table;
+ var ids = Zotero.DB.columnQuery(sql);
- var keyIDs = {};
- for each(var row in rows) {
- keyIDs[row.key] = row.id;
- }
-
- var store = {};
- for each(var obj in this._objectCache) {
- store[keyIDs[obj.key]] = obj;
+ for (var id in this._objectCache) {
+ if (!ids || ids.indexOf(id) == -1) {
+ delete this._objectCache[id];
+ }
}
- this._objectCache = store;
-
// Reload data
this._reloadCache = true;
this._load();
@@ -166,11 +236,16 @@ Zotero.DataObjects = function (object, objectPlural, id, table) {
var numDiffs = 0;
var subs = ['primary', 'fields'];
+ var skipFields = ['collectionID', 'creatorID', 'itemID', 'searchID', 'tagID', 'libraryID', 'key'];
for each(var sub in subs) {
diff[0][sub] = {};
diff[1][sub] = {};
for (var field in data1[sub]) {
+ if (skipFields.indexOf(field) != -1) {
+ continue;
+ }
+
if (!data1[sub][field] && !data2[sub][field]) {
continue;
}
@@ -192,6 +267,10 @@ Zotero.DataObjects = function (object, objectPlural, id, table) {
// DEBUG: some of this is probably redundant
for (var field in data2[sub]) {
+ if (skipFields.indexOf(field) != -1) {
+ continue;
+ }
+
if (diff[0][sub][field] != undefined) {
continue;
}
@@ -218,5 +297,39 @@ Zotero.DataObjects = function (object, objectPlural, id, table) {
return numDiffs;
}
+
+
+ this.isEditable = function (obj) {
+ var libraryID = obj.libraryID;
+ if (!libraryID) {
+ return true;
+ }
+
+ var type = Zotero.Libraries.getType(libraryID);
+ switch (type) {
+ case 'group':
+ var groupID = Zotero.Groups.getGroupIDFromLibraryID(libraryID);
+ var group = Zotero.Groups.get(groupID);
+ if (!group.editable) {
+ return false;
+ }
+ if (obj.objectType == 'item' && obj.isAttachment()
+ && (obj.attachmentLinkMode == Zotero.Attachments.LINK_MODE_IMPORTED_URL ||
+ obj.attachmentLinkMode == Zotero.Attachments.LINK_MODE_IMPORTED_FILE)) {
+ return group.filesEditable;
+ }
+ return true;
+
+ default:
+ throw ("Unsupported library type '" + type + "' in Zotero.DataObjects.isEditable()");
+ }
+ }
+
+
+ this.editCheck = function (obj) {
+ if (!Zotero.Sync.Server.syncInProgress && !this.isEditable(obj)) {
+ throw ("Cannot edit " + this._ZDO_object + " in read-only Zotero library");
+ }
+ }
}
diff --git a/chrome/content/zotero/xpcom/data/group.js b/chrome/content/zotero/xpcom/data/group.js
@@ -0,0 +1,470 @@
+/*
+ ***** 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 *****
+*/
+
+
+Zotero.Group = function () {
+ if (arguments[0]) {
+ throw ("Zotero.Group constructor doesn't take any parameters");
+ }
+
+ this._init();
+}
+
+
+Zotero.Group.prototype._init = function () {
+ this._id = null;
+ this._libraryID = null;
+ this._name = null;
+ this._description = null;
+ this._editable = null;
+ this._filesEditable = null;
+
+ this._loaded = false;
+ this._changed = false;
+ this._hasCollections = null;
+}
+
+
+Zotero.Group.prototype.__defineGetter__('objectType', function () { return 'group'; });
+Zotero.Group.prototype.__defineGetter__('id', function () { return this._get('id'); });
+Zotero.Group.prototype.__defineSetter__('id', function (val) { this._set('id', val); });
+Zotero.Group.prototype.__defineGetter__('libraryID', function () { return this._get('libraryID'); });
+Zotero.Group.prototype.__defineSetter__('libraryID', function (val) { return this._set('libraryID', val); });
+Zotero.Group.prototype.__defineGetter__('name', function () { return this._get('name'); });
+Zotero.Group.prototype.__defineSetter__('name', function (val) { this._set('name', val); });
+Zotero.Group.prototype.__defineGetter__('description', function () { return this._get('description'); });
+Zotero.Group.prototype.__defineSetter__('description', function (val) { this._set('description', val); });
+Zotero.Group.prototype.__defineGetter__('editable', function () { return this._get('editable'); });
+Zotero.Group.prototype.__defineSetter__('editable', function (val) { this._set('editable', val); });
+Zotero.Group.prototype.__defineGetter__('filesEditable', function () { return this._get('filesEditable'); });
+Zotero.Group.prototype.__defineSetter__('filesEditable', function (val) { this._set('filesEditable', val); });
+
+
+Zotero.Group.prototype._get = function (field) {
+ if (this._id && !this._loaded) {
+ this.load();
+ }
+ return this['_' + field];
+}
+
+
+Zotero.Group.prototype._set = function (field, val) {
+ switch (field) {
+ case 'id':
+ case 'libraryID':
+ if (val == this['_' + field]) {
+ return;
+ }
+
+ if (this._loaded) {
+ throw ("Cannot set " + field + " after object is already loaded in Zotero.Group._set()");
+ }
+ //this._checkValue(field, val);
+ this['_' + field] = val;
+ return;
+ }
+
+ if (this.id) {
+ if (!this._loaded) {
+ this.load();
+ }
+ }
+ else {
+ this._loaded = true;
+ }
+
+ if (this['_' + field] != val) {
+ this._prepFieldChange(field);
+
+ switch (field) {
+ default:
+ this['_' + field] = val;
+ }
+ }
+}
+
+
+
+
+
+/*
+ * Build group from database
+ */
+Zotero.Group.prototype.load = function() {
+ var id = this._id;
+
+ if (!id) {
+ throw ("ID not set in Zotero.Group.load()");
+ }
+
+ var sql = "SELECT G.* FROM groups G WHERE groupID=?";
+ var data = Zotero.DB.rowQuery(sql, id);
+
+ this._loaded = true;
+
+ if (!data) {
+ return;
+ }
+
+ this.loadFromRow(data);
+}
+
+
+/*
+ * Populate group data from a database row
+ */
+Zotero.Group.prototype.loadFromRow = function(row) {
+ this._loaded = true;
+ this._changed = false;
+ this._hasCollections = null;
+
+ this._id = row.groupID;
+ this._libraryID = row.libraryID;
+ this._name = row.name;
+ this._description = row.description;
+ this._editable = row.editable;
+ this._filesEditable = row.filesEditable;
+}
+
+
+/**
+ * Check if group exists in the database
+ *
+ * @return bool TRUE if the group exists, FALSE if not
+ */
+Zotero.Group.prototype.exists = function() {
+ if (!this.id) {
+ throw ('groupID not set in Zotero.Group.exists()');
+ }
+
+ var sql = "SELECT COUNT(*) FROM groups WHERE groupID=?";
+ return !!Zotero.DB.valueQuery(sql, this.id);
+}
+
+
+Zotero.Group.prototype.hasCollections = function () {
+ if (this._hasCollections !== null) {
+ return this._hasCollections;
+ }
+
+ this._hasCollections = !!this.getCollections().length;
+ return this._hasCollections;
+}
+
+
+Zotero.Group.prototype.clearCollectionsCache = function () {
+ this._hasCollections = null;
+}
+
+
+/**
+ * Returns collections of this group
+ *
+ * @param {Boolean} asIDs Return as collectionIDs
+ * @return {Zotero.Collection[]} Array of Zotero.Collection instances
+ */
+Zotero.Group.prototype.getCollections = function (parent) {
+ var sql = "SELECT collectionID FROM collections WHERE libraryID=? AND "
+ + "parentCollectionID " + (parent ? '=' + parent : 'IS NULL');
+ var ids = Zotero.DB.columnQuery(sql, this.libraryID);
+ Zotero.debug(ids);
+
+ // Return Zotero.Collection objects
+ var objs = [];
+ for each(var id in ids) {
+ var col = Zotero.Collections.get(id);
+ objs.push(col);
+ }
+ return objs;
+}
+
+
+Zotero.Group.prototype.hasItem = function (itemID) {
+ var item = Zotero.Items.get(itemID);
+ return item.libraryID == this.libraryID;
+}
+
+
+Zotero.Group.prototype.save = function () {
+ if (!this.id) {
+ throw ("ID not set in Zotero.Group.save()");
+ }
+
+ if (!this.libraryID) {
+ throw ("libraryID not set in Zotero.Group.save()");
+ }
+
+ if (!this._changed) {
+ Zotero.debug("Group " + this.id + " has not changed");
+ return false;
+ }
+
+ Zotero.DB.beginTransaction();
+
+ var isNew = !this.exists();
+
+ try {
+ Zotero.debug("Saving group " + this.id);
+
+ var columns = [
+ 'groupID',
+ 'libraryID',
+ 'name',
+ 'description',
+ 'editable',
+ 'filesEditable'
+ ];
+ var placeholders = ['?', '?', '?', '?', '?', '?'];
+ var sqlValues = [
+ this.id,
+ this.libraryID,
+ this.name,
+ this.description,
+ this.editable ? 1 : 0,
+ this.filesEditable ? 1 : 0
+ ];
+
+ if (isNew) {
+ if (!Zotero.Libraries.exists(this.libraryID)) {
+ Zotero.Libraries.add(this.libraryID, 'group');
+ }
+
+ var sql = "INSERT INTO groups (" + columns.join(', ') + ") "
+ + "VALUES (" + placeholders.join(', ') + ")";
+ Zotero.DB.query(sql, sqlValues);
+ }
+ else {
+ columns.shift();
+ sqlValues.shift();
+
+ var sql = "UPDATE groups SET "
+ + columns.map(function (val) val + '=?').join(', ')
+ + " WHERE groupID=?";
+ sqlValues.push(this.id);
+ Zotero.DB.query(sql, sqlValues);
+ }
+
+ Zotero.DB.commitTransaction();
+ }
+ catch (e) {
+ Zotero.DB.rollbackTransaction();
+ throw (e);
+ }
+
+ //Zotero.Groups.reload(this.id);
+
+ Zotero.Notifier.trigger('add', 'group', this.id);
+}
+
+
+/**
+* Deletes group and all descendant objects
+**/
+Zotero.Group.prototype.erase = function(deleteItems) {
+ Zotero.DB.beginTransaction();
+
+ var sql, ids, obj;
+
+ // Delete items
+ sql = "SELECT itemID FROM items WHERE libraryID=?";
+ ids = Zotero.DB.columnQuery(sql, this.libraryID);
+ Zotero.Items.erase(ids);
+
+ // Delete collections
+ sql = "SELECT collectionID FROM collections WHERE libraryID=?";
+ ids = Zotero.DB.columnQuery(sql, this.libraryID);
+ for each(var id in ids) {
+ obj = Zotero.Collections.get(id);
+ // Subcollections might've already been deleted
+ if (obj) {
+ obj.erase();
+ }
+ }
+
+ // Delete creators
+ sql = "SELECT creatorID FROM creators WHERE libraryID=?";
+ ids = Zotero.DB.columnQuery(sql, this.libraryID);
+ for each(var id in ids) {
+ obj = Zotero.Creators.get(id);
+ obj.erase();
+ }
+
+ // Delete saved searches
+ sql = "SELECT savedSearchID FROM savedSearches WHERE libraryID=?";
+ ids = Zotero.DB.columnQuery(sql, this.libraryID);
+ for each(var id in ids) {
+ obj = Zotero.Searches.get(id);
+ obj.erase();
+ }
+
+ // Delete tags
+ sql = "SELECT tagID FROM tags WHERE libraryID=?";
+ ids = Zotero.DB.columnQuery(sql, this.libraryID);
+ Zotero.Tags.erase(ids);
+
+ // Delete delete log entries
+ sql = "DELETE FROM syncDeleteLog WHERE libraryID=?";
+ Zotero.DB.query(sql, this.libraryID);
+
+ // Delete group
+ sql = "DELETE FROM groups WHERE groupID=?";
+ ids = Zotero.DB.query(sql, this.id)
+
+ Zotero.purgeDataObjects();
+
+ var notifierData = {};
+ notifierData[this.id] = this.serialize();
+
+ Zotero.DB.commitTransaction();
+
+ Zotero.Notifier.trigger('delete', 'group', this.id, notifierData);
+}
+
+
+Zotero.Group.prototype.serialize = function() {
+ var obj = {
+ primary: {
+ groupID: this.id,
+ libraryID: this.libraryID
+ },
+ fields: {
+ name: this.name,
+ description: this.description,
+ editable: this.editable,
+ filesEditable: this.filesEditable
+ }
+ };
+ return obj;
+}
+
+
+/**
+ * Returns an array of descendent groups and items
+ *
+ * @param bool recursive Descend into subgroups
+ * @param bool nested Return multidimensional array with 'children'
+ * nodes instead of flat array
+ * @param string type 'item', 'group', or FALSE for both
+ * @return {Object[]} Array of objects with 'id', 'key',
+ * 'type' ('item' or 'group'), 'parent',
+ * and, if group, 'name' and the nesting 'level'
+ */
+Zotero.Group.prototype.getChildren = function(recursive, nested, type, level) {
+ if (!this.id) {
+ throw ('Zotero.Group.getChildren() cannot be called on an unsaved item');
+ }
+
+ var toReturn = [];
+
+ if (!level) {
+ level = 1;
+ }
+
+ // 0 == group
+ // 1 == item
+ var children = Zotero.DB.query('SELECT groupID AS id, '
+ + "0 AS type, groupName AS groupName, key "
+ + 'FROM groups WHERE parentGroupID=?1'
+ + ' UNION SELECT itemID AS id, 1 AS type, NULL AS groupName, key '
+ + 'FROM groupItems JOIN items USING (itemID) WHERE groupID=?1', this.id);
+
+ if (type) {
+ switch (type) {
+ case 'item':
+ case 'group':
+ break;
+ default:
+ throw ("Invalid type '" + type + "' in Group.getChildren()");
+ }
+ }
+
+ 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=='group') {
+ toReturn.push({
+ id: children[i].id,
+ name: children[i].groupName,
+ key: children[i].key,
+ type: 'group',
+ level: level,
+ parent: this.id
+ });
+ }
+
+ if (recursive) {
+ var descendents =
+ Zotero.Groups.get(children[i].id).
+ getChildren(true, 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,
+ key: children[i].key,
+ type: 'item',
+ parent: this.id
+ });
+ }
+ break;
+ }
+ }
+
+ return toReturn;
+}
+
+
+/**
+ * Alias for the recursive mode of getChildren()
+ */
+Zotero.Group.prototype.getDescendents = function(nested, type, level) {
+ return this.getChildren(true, nested, type);
+}
+
+
+Zotero.Group.prototype._prepFieldChange = function (field) {
+ if (!this._changed) {
+ this._changed = {};
+ }
+ this._changed[field] = true;
+
+ // Save a copy of the data before changing
+ // TODO: only save previous data if group exists
+ if (this.id && this.exists() && !this._previousData) {
+ //this._previousData = this.serialize();
+ }
+}
diff --git a/chrome/content/zotero/xpcom/data/groups.js b/chrome/content/zotero/xpcom/data/groups.js
@@ -0,0 +1,58 @@
+Zotero.Groups = new function () {
+ this.__defineGetter__('addGroupURL', function () ZOTERO_CONFIG.WWW_BASE_URL + 'groups/new/');
+
+ this.get = function (id) {
+ if (!id) {
+ throw ("groupID not provided in Zotero.Groups.get()");
+ }
+ var group = new Zotero.Group;
+ group.id = id;
+ if (!group.exists()) {
+ return false;
+ }
+ return group;
+ }
+
+
+ this.getAll = function () {
+ var groups = [];
+ var sql = "SELECT groupID FROM groups";
+ var groupIDs = Zotero.DB.columnQuery(sql);
+ if (!groupIDs) {
+ return groups;
+ }
+ for each(var groupID in groupIDs) {
+ var group = this.get(groupID);
+ groups.push(group);
+ }
+ return groups;
+ }
+
+
+ this.getByLibraryID = function (libraryID) {
+ var groupID = this.getGroupIDFromLibraryID(libraryID);
+ return this.get(groupID);
+ }
+
+
+ this.getGroupIDFromLibraryID = function (libraryID) {
+ var sql = "SELECT groupID FROM groups WHERE libraryID=?";
+ var groupID = Zotero.DB.valueQuery(sql, libraryID);
+ if (!groupID) {
+ throw ("Group with libraryID " + libraryID + " does not exist "
+ + "in Zotero.Groups.getGroupIDFromLibraryID()");
+ }
+ return groupID;
+ }
+
+
+ this.getLibraryIDFromGroupID = function (groupID) {
+ var sql = "SELECT libraryID FROM groups WHERE groupID=?";
+ var libraryID = Zotero.DB.valueQuery(sql, groupID);
+ if (!libraryID) {
+ throw ("Group with groupID " + groupID + " does not exist "
+ + "in Zotero.Groups.getLibraryIDFromGroupID()");
+ }
+ return libraryID;
+ }
+}
diff --git a/chrome/content/zotero/xpcom/data/item.js b/chrome/content/zotero/xpcom/data/item.js
@@ -26,26 +26,14 @@
*
* Generally should be called through Zotero.Items rather than directly
*/
-Zotero.Item = function(itemID, itemTypeOrID) {
- if (!this._init) {
- throw ('Zotero.Item() cannot be called statically');
+Zotero.Item = function(itemTypeOrID) {
+ if (arguments[1] || arguments[2]) {
+ throw ("Zotero.Item constructor only takes one parameter");
}
- //
- // These members are public so that they can be accessed by public methods
- // -- do not access directly
- //
-
this._disabled = false;
this._init();
- if (itemID) {
- if (itemID != parseInt(itemID)) {
- throw ("Invalid itemID '" + itemID + "' in Zotero.Item(itemID, itemTypeOrID)");
- }
- this._itemID = parseInt(itemID);
- }
-
if (itemTypeOrID) {
// setType initializes type-specific properties in this._itemData
this.setType(Zotero.ItemTypes.getID(itemTypeOrID));
@@ -54,17 +42,19 @@ Zotero.Item = function(itemID, itemTypeOrID) {
Zotero.Item.prototype._init = function () {
// Primary fields
+ this._id = null;
+ this._libraryID = null
+ this._key = null;
this._itemTypeID = null;
this._dateAdded = null;
this._dateModified = null;
- this._key = null;
this._firstCreator = null;
this._numNotes = null;
this._numAttachments = null;
this._creators = [];
this._itemData = null;
- this._sourceItemID = null;
+ this._sourceItem = null;
this._primaryDataLoaded = false;
this._creatorsLoaded = false;
@@ -97,29 +87,31 @@ Zotero.Item.prototype._init = function () {
}
-Zotero.Item.prototype.__defineGetter__('id', function () { return this._itemID; });
-Zotero.Item.prototype.__defineGetter__('itemID', function () { return this._itemID; });
-Zotero.Item.prototype.__defineSetter__('itemID', function (val) { return this.setField('itemID', val); }); // used by sync.js
+Zotero.Item.prototype.__defineGetter__('objectType', function () { return 'item'; });
+Zotero.Item.prototype.__defineGetter__('id', function () { return this.getField('id'); });
+Zotero.Item.prototype.__defineGetter__('itemID', function () {
+ Zotero.debug("Item.itemID is deprecated -- use Item.id");
+ return this.id;
+});
+Zotero.Item.prototype.__defineSetter__('id', function (val) { this.setField('id', val); });
+Zotero.Item.prototype.__defineGetter__('libraryID', function () { return this.getField('libraryID'); });
+Zotero.Item.prototype.__defineSetter__('libraryID', function (val) { this.setField('libraryID', val); });
+Zotero.Item.prototype.__defineGetter__('key', function () { return this.getField('key'); });
+Zotero.Item.prototype.__defineSetter__('key', function (val) { this.setField('key', val) });
Zotero.Item.prototype.__defineGetter__('itemTypeID', function () { return this.getField('itemTypeID'); });
Zotero.Item.prototype.__defineGetter__('dateAdded', function () { return this.getField('dateAdded'); });
Zotero.Item.prototype.__defineGetter__('dateModified', function () { return this.getField('dateModified'); });
-Zotero.Item.prototype.__defineGetter__('key', function () { return this.getField('key'); });
-Zotero.Item.prototype.__defineSetter__('key', function (val) { return this.setField('key', val); }); // used by sync.js
Zotero.Item.prototype.__defineGetter__('firstCreator', function () { return this.getField('firstCreator'); });
-//Zotero.Item.prototype.__defineGetter__('numNotes', function () { return this._itemID; });
-//Zotero.Item.prototype.__defineGetter__('numAttachments', function () { return this._itemID; });
Zotero.Item.prototype.__defineGetter__('relatedItems', function () { var ids = this._getRelatedItems(true); return ids ? ids : []; });
Zotero.Item.prototype.__defineSetter__('relatedItems', function (arr) { this._setRelatedItems(arr); });
Zotero.Item.prototype.__defineGetter__('relatedItemsReverse', function () { var ids = this._getRelatedItemsReverse(); return ids ? ids : []; });
Zotero.Item.prototype.__defineGetter__('relatedItemsBidirectional', function () { var ids = this._getRelatedItemsBidirectional(); return ids ? ids : []; });
-/*
- * Deprecated -- use id property
- */
+
Zotero.Item.prototype.getID = function() {
Zotero.debug('Item.getID() is deprecated -- use Item.id');
- return this._itemID;
+ return this.id;
}
Zotero.Item.prototype.getType = function() {
@@ -127,29 +119,17 @@ Zotero.Item.prototype.getType = function() {
return this.getField('itemTypeID');
}
+Zotero.Item.prototype.isPrimaryField = function (fieldName) {
+ Zotero.debug("Zotero.Item.isPrimaryField() is deprecated -- use Zotero.Items.isPrimaryField()");
+ return Zotero.Items.isPrimaryField(fieldName);
+}
+
//////////////////////////////////////////////////////////////////////////////
//
// 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];
-}
-
-
/**
* Check if item exists in the database
*
@@ -177,14 +157,20 @@ Zotero.Item.prototype.exists = function() {
* type-specific field instead (e.g. 'label' for 'publisher' in 'audioRecording')
*/
Zotero.Item.prototype.getField = function(field, unformatted, includeBaseMapped) {
- this._disabledCheck();
+ // We don't allow access after saving to force use of the centrally cached
+ // object, but we make an exception for the id
+ if (field != 'id') {
+ this._disabledCheck();
+ }
+
+ //Zotero.debug('Requesting field ' + field + ' for item ' + this._id, 4);
+
+ if ((this._id || this._key) && !this._primaryDataLoaded) {
+ this.loadPrimaryData(true);
+ }
- //Zotero.debug('Requesting field ' + field + ' for item ' + this.id, 4);
- if (this.isPrimaryField(field)) {
+ if (field == 'id' || Zotero.Items.isPrimaryField(field)) {
var privField = '_' + field;
- if (this.id && !this._primaryDataLoaded) {
- this.loadPrimaryData(true);
- }
//Zotero.debug('Returning ' + (this[privField] ? this[privField] : '') + ' (typeof ' + typeof this[privField] + ')');
return this[privField];
}
@@ -231,12 +217,23 @@ Zotero.Item.prototype.getField = function(field, unformatted, includeBaseMapped)
}
+/**
+ * @param {Boolean} asNames
+ * @return {Integer{}|String[]}
+ */
Zotero.Item.prototype.getUsedFields = function(asNames) {
+ if (!this.id) {
+ return [];
+ }
var sql = "SELECT fieldID FROM itemData WHERE itemID=?";
if (asNames) {
sql = "SELECT fieldName FROM fields WHERE fieldID IN (" + sql + ")";
}
- return Zotero.DB.columnQuery(sql, this.id);
+ var fields = Zotero.DB.columnQuery(sql, this.id);
+ if (!fields) {
+ return [];
+ }
+ return fields;
}
@@ -245,18 +242,24 @@ Zotero.Item.prototype.getUsedFields = function(asNames) {
* Build object from database
*/
Zotero.Item.prototype.loadPrimaryData = function(allowFail) {
- if (!this.id) {
- throw ('ID not set in Zotero.Item.loadPrimaryData()');
+ var id = this._id;
+ var key = this._key;
+ var libraryID = this._libraryID;
+
+ if (!id && !key) {
+ throw ('ID or key not set in Zotero.Item.loadPrimaryData()');
}
var columns = [], join = [], where = [];
- for (var field in Zotero.Item.primaryFields) {
+ for each(var field in Zotero.Items.primaryFields) {
var colSQL = null, joinSQL = null, whereSQL = null;
+
// If field not already set
- if (this['_' + field] === null) {
+ if (field == 'itemID' || this['_' + field] === null) {
// Parts should be the same as query in Zotero.Items._load, just
// without itemID clause
switch (field) {
+ case 'itemID':
case 'itemTypeID':
case 'dateAdded':
case 'dateModified':
@@ -290,19 +293,42 @@ Zotero.Item.prototype.loadPrimaryData = function(allowFail) {
where.push(whereSQL);
}
}
+ else {
+ Zotero.debug("skipping " + field);
+ }
+ }
+
+ if (!columns.length) {
+ throw ("No columns to load in Zotero.Item.loadPrimaryData()");
}
- var sql = 'SELECT I.itemID' + (columns.length ? ', ' + columns.join(', ') : '')
- + " FROM items I " + (join.length ? join.join(' ') + ' ' : '')
- + "WHERE I.itemID=?" + (where.length ? ' AND ' + where.join(' AND ') : '');
- var row = Zotero.DB.rowQuery(sql, this.id);
+ var sql = 'SELECT ' + columns.join(', ') + " FROM items I "
+ + (join.length ? join.join(' ') + ' ' : '') + "WHERE ";
+ if (id) {
+ sql += "itemID=? ";
+ var params = id;
+ }
+ else {
+ sql += "key=? ";
+ var params = [key];
+ if (libraryID) {
+ sql += "AND libraryID=? ";
+ params.push(libraryID);
+ }
+ else {
+ sql += "AND libraryID IS NULL ";
+ }
+ }
+ sql += (where.length ? ' AND ' + where.join(' AND ') : '');
+ var row = Zotero.DB.rowQuery(sql, params);
if (!row) {
if (allowFail) {
this._primaryDataLoaded = true;
return false;
}
- throw ("Item " + this.id + " not found in Zotero.Item.loadPrimaryData()");
+ throw ("Item " + (id ? id : libraryID + "/" + key)
+ + " not found in Zotero.Item.loadPrimaryData()");
}
this.loadFromRow(row);
@@ -325,10 +351,22 @@ Zotero.Item.prototype.loadFromRow = function(row, reload) {
}
for (var col in row) {
+ if (col == 'clientDateModified') {
+ continue;
+ }
+
// Only accept primary field data through loadFromRow()
- if (this.isPrimaryField(col)) {
+ if (Zotero.Items.isPrimaryField(col)) {
//Zotero.debug("Setting field '" + col + "' to '" + row[col] + "' for item " + this.id);
switch (col) {
+ case 'itemID':
+ this._id = row[col];
+ break;
+
+ case 'libraryID':
+ this['_' + col] = row[col] ? row[col] : null;
+ break;
+
case 'numNotes':
case 'numAttachments':
this['_' + col] = row[col] ? parseInt(row[col]) : 0;
@@ -565,23 +603,44 @@ Zotero.Item.prototype.setField = function(field, value, loadIn) {
throw ("Field not specified in Item.setField()");
}
+ // Set id, libraryID, and key without loading data first
+ switch (field) {
+ case 'id':
+ case 'libraryID':
+ case 'key':
+ if (value == this['_' + field]) {
+ return;
+ }
+
+ if (this._primaryDataLoaded) {
+ throw ("Cannot set " + field + " after object is already loaded in Zotero.Item.setField()");
+ }
+ //this._checkValue(field, val);
+ this['_' + field] = value;
+ return;
+ }
+
+ if (this._id || this._key) {
+ if (!this._primaryDataLoaded) {
+ this.loadPrimaryData(true);
+ }
+ }
+ else {
+ this._primaryDataLoaded = true;
+ }
+
// Primary field
- if (this.isPrimaryField(field)) {
+ if (Zotero.Items.isPrimaryField(field)) {
+ if (loadIn) {
+ throw('Cannot set primary field ' + field + ' in loadIn mode in Zotero.Item.setField()');
+ }
+
switch (field) {
- //case 'itemID': // necessary for id changes during sync
+ case 'itemID':
case 'firstCreator':
case 'numNotes':
case 'numAttachments':
- throw ('Primary field ' + field + ' cannot be changed through setField()');
- }
-
- if (this.id) {
- if (!this._primaryDataLoaded) {
- this.loadPrimaryData(true);
- }
- }
- else {
- this._primaryDataLoaded = true;
+ throw ('Primary field ' + field + ' cannot be changed in Zotero.Item.setField()');
}
/*
@@ -590,11 +649,6 @@ Zotero.Item.prototype.setField = function(field, value, loadIn) {
}
*/
- if (loadIn) {
- // allowed?
- throw('Cannot set primary field through setField() in loadIn mode');
- }
-
// If field value has changed
if (this['_' + field] != value) {
Zotero.debug("Field '" + field + "' has changed from '" + this['_' + field] + "' to '" + value + "'", 4);
@@ -944,7 +998,7 @@ Zotero.Item.prototype.__defineGetter__('deleted', function () {
}
if (!this.id) {
- return '';
+ return false;
}
var sql = "SELECT COUNT(*) FROM deletedItems WHERE itemID=?";
@@ -955,13 +1009,6 @@ Zotero.Item.prototype.__defineGetter__('deleted', function () {
Zotero.Item.prototype.__defineSetter__('deleted', function (val) {
- Zotero.debug('setting deleted');
- Zotero.debug(val);
- if (!this.id) {
- Zotero.debug("Deleted state not set on item without id");
- return;
- }
-
var deleted = !!val;
if (this.deleted == deleted) {
@@ -1045,6 +1092,8 @@ Zotero.Item.prototype.removeRelatedItem = function (itemID) {
* Returns true on item update or itemID of new item
*/
Zotero.Item.prototype.save = function() {
+ Zotero.Items.editCheck(this);
+
if (!this.hasChanged()) {
Zotero.debug('Item ' + this.id + ' has not changed', 4);
return false;
@@ -1064,45 +1113,6 @@ Zotero.Item.prototype.save = function() {
Zotero.DB.beginTransaction();
- // ID change
- if (this._changedPrimaryData && this._changedPrimaryData.itemID) {
- // Foreign key constraints, how lovely you would be
- var oldID = this._previousData.primary.itemID;
- var params = [this.id, oldID];
-
- Zotero.debug("Changing itemID " + oldID + " to " + this.id);
-
- var row = Zotero.DB.rowQuery("SELECT * FROM items WHERE itemID=?", oldID);
- // Add a new row so we can update the old rows despite FK checks
- // Use temp key due to UNIQUE constraint on key column
- Zotero.DB.query("INSERT INTO items VALUES (?, ?, ?, ?, ?)",
- [this.id, row.itemTypeID, row.dateAdded, row.dateModified, 'TEMPKEY']);
-
- Zotero.DB.query("UPDATE collectionItems SET itemID=? WHERE itemID=?", params);
- Zotero.DB.query("UPDATE itemCreators SET itemID=? WHERE itemID=?", params);
- Zotero.DB.query("UPDATE itemAttachments SET itemID=? WHERE itemID=?", params);
- Zotero.DB.query("UPDATE itemAttachments SET sourceItemID=? WHERE sourceItemID=?", params);
- Zotero.DB.query("UPDATE itemData SET itemID=? WHERE itemID=?", params);
- Zotero.DB.query("UPDATE itemNotes SET itemID=? WHERE itemID=?", params);
- Zotero.DB.query("UPDATE itemNotes SET sourceItemID=? WHERE sourceItemID=?", params);
- Zotero.DB.query("UPDATE itemSeeAlso SET itemID=? WHERE itemID=?", params);
- Zotero.DB.query("UPDATE itemSeeAlso SET linkedItemID=? WHERE linkedItemID=?", params);
- Zotero.DB.query("UPDATE itemTags SET itemID=? WHERE itemID=?", params);
- Zotero.DB.query("UPDATE fulltextItemWords SET itemID=? WHERE itemID=?", params);
- Zotero.DB.query("UPDATE fulltextItems SET itemID=? WHERE itemID=?", params);
- Zotero.DB.query("UPDATE deletedItems SET itemID=? WHERE itemID=?", params);
- Zotero.DB.query("UPDATE annotations SET itemID=? WHERE itemID=?", params);
- Zotero.DB.query("UPDATE highlights SET itemID=? WHERE itemID=?", params);
-
- Zotero.DB.query("DELETE FROM items WHERE itemID=?", oldID);
- Zotero.DB.query("UPDATE items SET key=? WHERE itemID=?", [row.key, this.id]);
-
- Zotero.Notifier.trigger('id-change', 'item', oldID + '-' + this.id);
-
- // update caches
- }
-
-
var isNew = !this.id || !this.exists();
try {
@@ -1128,12 +1138,22 @@ Zotero.Item.prototype.save = function() {
var key = this.key ? this.key : this._generateKey();
- sqlColumns.push('itemTypeID', 'key');
- sqlValues.push({ int: this.getField('itemTypeID') }, key);
- sqlColumns.push('dateAdded');
- sqlValues.push(this.dateAdded ? this.dateAdded : Zotero.DB.transactionDateTime);
- sqlColumns.push('dateModified');
- sqlValues.push(this.dateModified ? this.dateModified : Zotero.DB.transactionDateTime);
+ sqlColumns.push(
+ 'itemTypeID',
+ 'dateAdded',
+ 'dateModified',
+ 'clientDateModified',
+ 'libraryID',
+ 'key'
+ );
+ sqlValues.push(
+ { int: this.getField('itemTypeID') },
+ this.dateAdded ? this.dateAdded : Zotero.DB.transactionDateTime,
+ this.dateModified ? this.dateModified : Zotero.DB.transactionDateTime,
+ Zotero.DB.transactionDateTime,
+ this.libraryID ? this.libraryID : null,
+ key
+ );
// Begin history transaction
// No associated id yet, so we use false
@@ -1346,11 +1366,17 @@ Zotero.Item.prototype.save = function() {
// Parent item
- if (this._sourceItemID) {
- var newSourceItem = Zotero.Items.get(this._sourceItemID);
+ if (this._sourceItem) {
+ if (typeof this._sourceItem == 'number') {
+ var newSourceItem = Zotero.Items.get(this._sourceItem);
+ }
+ else {
+ var newSourceItem = Zotero.Items.getByLibraryAndKey(this.libraryID, this._sourceItem);
+ }
+
if (!newSourceItem) {
// TODO: clear caches?
- throw ("Cannot set source to invalid item " + this._sourceItemID);
+ throw ("Cannot set source to invalid item " + this._sourceItem);
}
var newSourceItemNotifierData = {};
@@ -1439,13 +1465,20 @@ Zotero.Item.prototype.save = function() {
var sql = "UPDATE items SET ";
var sqlValues = [];
- var updateFields = ['itemTypeID', 'key', 'dateAdded', 'dateModified'];
+ var updateFields = [
+ 'itemTypeID',
+ 'dateAdded',
+ 'dateModified',
+ 'clientDateModified',
+ 'libraryID',
+ 'key'
+ ];
for each(field in updateFields) {
if (this._changedPrimaryData && this._changedPrimaryData[field]) {
sql += field + '=?, ';
sqlValues.push(this.getField(field));
}
- else if (field == 'dateModified') {
+ else if (field == 'dateModified' || field == 'clientDateModified') {
sql += field + '=?, ';
sqlValues.push(Zotero.DB.transactionDateTime);
}
@@ -1703,33 +1736,44 @@ Zotero.Item.prototype.save = function() {
var type = Zotero.ItemTypes.getName(this.itemTypeID);
var Type = type[0].toUpperCase() + type.substr(1);
- if (this._sourceItemID) {
- var newSourceItem = Zotero.Items.get(this._sourceItemID);
+ if (this._sourceItem) {
+ if (typeof this._sourceItem == 'number') {
+ var newSourceItem = Zotero.Items.get(this._sourceItem);
+ }
+ else {
+ var newSourceItem = Zotero.Items.getByLibraryAndKey(this.libraryID, this._sourceItem);
+ }
+
if (!newSourceItem) {
// TODO: clear caches
- throw ("Cannot set source to invalid item " + this._sourceItemID);
+ throw ("Cannot set source to invalid item " + this._sourceItem);
}
}
if (newSourceItem) {
var newSourceItemNotifierData = {};
- newSourceItemNotifierData[this._sourceItemID] =
+ newSourceItemNotifierData[newSourceItem.id] =
{ old: newSourceItem.serialize() };
}
if (this._previousData) {
- var oldSourceItemID = this._previousData.sourceItemID;
- if (oldSourceItemID) {
- var oldSourceItem = Zotero.Items.get(oldSourceItemID);
+ var oldSourceItemIDOrKey = this._previousData.sourceItem;
+ if (oldSourceItemIDOrKey) {
+ if (typeof oldSourceItemIDOrKey == 'number') {
+ var oldSourceItem = Zotero.Items.get(oldSourceItemIDOrKey);
+ }
+ else {
+ var oldSourceItem = Zotero.Items.getByLibraryAndKey(this.libraryID, oldSourceItemIDOrKey);
+ }
}
if (oldSourceItem) {
var oldSourceItemNotifierData = {};
- oldSourceItemNotifierData[oldSourceItemID] =
+ oldSourceItemNotifierData[oldSourceItem.id] =
{ old: oldSourceItem.serialize() };
}
- else if (oldSourceItemID) {
+ else if (oldSourceItemIDOrKey) {
var oldSourceItemNotifierData = null;
- Zotero.debug("Old source item " + oldSourceItemID
+ Zotero.debug("Old source item " + oldSourceItemIDOrKey
+ " didn't exist in setSource()", 2);
}
}
@@ -1738,15 +1782,15 @@ Zotero.Item.prototype.save = function() {
// If this was an independent item, remove from any collections
// where it existed previously and add source instead if
// there is one
- if (!oldSourceItemID) {
+ if (!oldSourceItemIDOrKey) {
var sql = "SELECT collectionID FROM collectionItems "
- + "WHERE itemID=?";
+ + "WHERE itemID=?";
var changedCollections = Zotero.DB.columnQuery(sql, this.id);
if (changedCollections) {
- if (this._sourceItemID) {
+ if (newSourceItem) {
sql = "UPDATE OR REPLACE collectionItems "
+ "SET itemID=? WHERE itemID=?";
- Zotero.DB.query(sql, [this._sourceItemID, this.id]);
+ Zotero.DB.query(sql, [newSourceItem.id, this.id]);
}
else {
sql = "DELETE FROM collectionItems WHERE itemID=?";
@@ -1761,10 +1805,9 @@ Zotero.Item.prototype.save = function() {
if (!this._changedAttachmentData &&
(!this._changedNote || !this.isNote())) {
var sql = "UPDATE item" + Type + "s SET sourceItemID=? "
- + "WHERE itemID=?";
+ + "WHERE itemID=?";
var bindParams = [
- this._sourceItemID ? { int: this._sourceItemID } : null,
- this.id
+ newSourceItem ? newSourceItem.id : null, this.id
];
Zotero.DB.query(sql, bindParams);
}
@@ -1854,11 +1897,12 @@ Zotero.Item.prototype.save = function() {
catch (e) {
//Zotero.History.cancel();
Zotero.DB.rollbackTransaction();
+ Zotero.debug(e);
throw(e);
}
if (!this.id) {
- this._itemID = itemID;
+ this._id = itemID;
}
if (!this.key) {
@@ -1887,7 +1931,7 @@ Zotero.Item.prototype.save = function() {
}
if (newSourceItem) {
Zotero.Notifier.trigger('modify', 'item',
- this._sourceItemID, newSourceItemNotifierData);
+ newSourceItem.id, newSourceItemNotifierData);
}
if (isNew) {
@@ -1905,17 +1949,32 @@ Zotero.Item.prototype.isRegularItem = function() {
}
+Zotero.Item.prototype.isTopLevelItem = function () {
+ return this.isRegularItem() || !this.getSource();
+}
+
+
Zotero.Item.prototype.numChildren = function(includeTrashed) {
return this.numNotes(includeTrashed) + this.numAttachments(includeTrashed);
}
/**
-* Get the itemID of the source item for a note or file
-**/
+ * Get the itemID of the source item for a note or file
+ * @return {Integer}
+ */
Zotero.Item.prototype.getSource = function() {
- if (this._sourceItemID !== null) {
- return this._sourceItemID;
+ if (this._sourceItem !== null) {
+ if (typeof this._sourceItem == 'number') {
+ return this._sourceItem;
+ }
+ var sourceItem = Zotero.Items.getByLibraryAndKey(this.libraryID, this._sourceItem);
+ if (!sourceItem) {
+ throw ("Source item for keyed source doesn't exist in Zotero.Item.getSource()");
+ }
+ // Replace stored key with id
+ this._sourceItem = sourceItem.id;
+ return sourceItem.id;
}
if (!this.id) {
@@ -1937,11 +1996,49 @@ Zotero.Item.prototype.getSource = function() {
if (!sourceItemID) {
sourceItemID = null;
}
- this._sourceItemID = sourceItemID;
+ this._sourceItem = sourceItemID;
return sourceItemID;
}
+/**
+ * Get the key of the source item for a note or file
+ * @return {String}
+ */
+Zotero.Item.prototype.getSourceKey = function() {
+ if (this._sourceItem !== null) {
+ if (typeof this._sourceItem == 'string') {
+ return this._sourceItem;
+ }
+ var sourceItem = Zotero.Items.get(this._sourceItem);
+ return sourceItem.key;
+ }
+
+ if (!this.id) {
+ return false;
+ }
+
+ if (this.isNote()) {
+ var Type = 'Note';
+ }
+ else if (this.isAttachment()) {
+ var Type = 'Attachment';
+ }
+ else {
+ return false;
+ }
+
+ var sql = "SELECT key FROM item" + Type + "s A JOIN items B "
+ + "ON (A.sourceItemID=B.itemID) WHERE A.itemID=?";
+ var key = Zotero.DB.valueQuery(sql, this.id);
+ if (!key) {
+ key = null;
+ }
+ this._sourceItem = key;
+ return key;
+}
+
+
Zotero.Item.prototype.setSource = function(sourceItemID) {
if (this.isNote()) {
var type = 'note';
@@ -1957,7 +2054,44 @@ Zotero.Item.prototype.setSource = function(sourceItemID) {
var oldSourceItemID = this.getSource();
if (oldSourceItemID == sourceItemID) {
- Zotero.debug("Source item has not changed in Zotero.Item.setSource()");
+ Zotero.debug("Source item has not changed for item " + this.id);
+ return false;
+ }
+
+ if (this.id && this.exists() && !this._previousData) {
+ this._previousData = this.serialize();
+ }
+
+ this._sourceItem = sourceItemID ? parseInt(sourceItemID) : null;
+ this._changedSource = true;
+
+ return true;
+}
+
+
+Zotero.Item.prototype.setSourceKey = function(sourceItemKey) {
+ if (this.isNote()) {
+ var type = 'note';
+ var Type = 'Note';
+ }
+ else if (this.isAttachment()) {
+ var type = 'attachment';
+ var Type = 'Attachment';
+ }
+ else {
+ throw ("setSourceKey() can only be called on items of type 'note' or 'attachment'");
+ }
+
+ var oldSourceItemID = this.getSource();
+ if (oldSourceItemID) {
+ var sourceItem = Zotero.Items.get(oldSourceItemID);
+ var oldSourceItemKey = sourceItem.key;
+ }
+ else {
+ var oldSourceItemKey = null;
+ }
+ if (oldSourceItemKey == sourceItemKey) {
+ Zotero.debug("Source item has not changed in Zotero.Item.setSourceKey()");
return false;
}
@@ -1965,7 +2099,7 @@ Zotero.Item.prototype.setSource = function(sourceItemID) {
this._previousData = this.serialize();
}
- this._sourceItemID = sourceItemID;
+ this._sourceItem = sourceItemKey ? sourceItemKey : null;
this._changedSource = true;
return true;
@@ -2216,6 +2350,18 @@ Zotero.Item.prototype.isAttachment = function() {
}
+Zotero.Item.prototype.isWebAttachment = function() {
+ if (!this.isAttachment()) {
+ return false;
+ }
+ var linkMode = this.attachmentLinkMode;
+ if (linkMode == Zotero.Attachments.LINK_MODE_IMPORTED_FILE || linkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE) {
+ return false;
+ }
+ return true;
+}
+
+
/**
* Returns number of child attachments of item
*
@@ -2437,7 +2583,7 @@ Zotero.Item.prototype.__defineGetter__('attachmentLinkMode', function () {
}
if (!this.id) {
- return '';
+ return null;
}
var sql = "SELECT linkMode FROM itemAttachments WHERE itemID=?";
@@ -2811,8 +2957,9 @@ Zotero.Item.prototype.addTag = function(name, type) {
}
Zotero.DB.beginTransaction();
+ try {
- var matchingTags = Zotero.Tags.getIDs(name);
+ var matchingTags = Zotero.Tags.getIDs(name, this.libraryID);
var itemTags = this.getTags();
if (matchingTags && itemTags) {
for each(var id in matchingTags) {
@@ -2835,20 +2982,22 @@ Zotero.Item.prototype.addTag = function(name, type) {
}
}
- var tagID = Zotero.Tags.getID(name, type);
+ var tagID = Zotero.Tags.getID(name, type, this.libraryID);
if (!tagID) {
var tag = new Zotero.Tag;
+ tag.libraryID = this.libraryID ? this.libraryID : null;
tag.name = name;
tag.type = type;
var tagID = tag.save();
}
- try {
- var added = this.addTagByID(tagID);
- Zotero.DB.commitTransaction();
- return added ? tagID : false;
+ var added = this.addTagByID(tagID);
+ Zotero.DB.commitTransaction();
+ return added ? tagID : false;
+
}
catch (e) {
+ Zotero.debug(e);
Zotero.DB.rollbackTransaction();
throw (e);
}
@@ -3018,6 +3167,59 @@ Zotero.Item.prototype.removeAllTags = function() {
}
+/**
+ * Return an item in the specified library equivalent to this item
+ */
+Zotero.Item.prototype.getLinkedItem = function (libraryID) {
+ if (libraryID == this.libraryID) {
+ throw ("Item is already in library " + libraryID + " in Zotero.Item.getLinkedItem()");
+ }
+
+ var predicate = Zotero.Items.linkedItemPredicate;
+ var itemURI = Zotero.URI.getItemURI(this);
+ var links = Zotero.Relations.getObject(itemURI, predicate, false).concat(
+ Zotero.Relations.getSubject(false, predicate, itemURI)
+ );
+ Zotero.debug(links);
+ if (!links.length) {
+ return false;
+ }
+
+ if (libraryID) {
+ var libraryItemPrefix = Zotero.URI.getLibraryURI(libraryID) + "/items/";
+ }
+ else {
+ var libraryItemPrefix = Zotero.URI.getCurrentUserURI() + "/items/";
+ }
+ for each(var link in links) {
+ if (link.indexOf(libraryItemPrefix) == 0) {
+ var item = Zotero.URI.getURIItem(link);
+ if (!item) {
+ Zotero.debug("Referenced linked item '" + link + "' not found in Zotero.Item.getLinkedItem()", 2);
+ continue;
+ }
+ return item;
+ }
+ }
+ return false;
+}
+
+
+Zotero.Item.prototype.addLinkedItem = function (item) {
+ var url1 = Zotero.URI.getItemURI(this);
+ var url2 = Zotero.URI.getItemURI(item);
+ var predicate = Zotero.Items.linkedItemPredicate;
+ if (Zotero.Relations.getByURIs(url1, predicate, url2).length
+ || Zotero.Relations.getByURIs(url2, predicate, url1).length) {
+ Zotero.debug("Items " + this.key + " and " + item.key + " are already linked");
+ return false;
+ }
+ Zotero.Relations.add(null, url1, predicate, url2);
+}
+
+
+
+
Zotero.Item.prototype.getImageSrc = function() {
var itemType = Zotero.ItemTypes.getName(this.itemTypeID);
if (itemType == 'attachment') {
@@ -3057,11 +3259,17 @@ Zotero.Item.prototype.getImageSrc = function() {
*
* @param {Zotero.Item} item Zotero.Item to compare this item to
* @param {Boolean} includeMatches Include all fields, even those that aren't different
- * @param {Boolean} ignoreOnlyDateModified If no fields other than dateModified
- * are different, just return false
+ * @param {Boolean} ignoreFields If no fields other than those specified
+ * are different, just return false --
+ * only works for primary fields
*/
-Zotero.Item.prototype.diff = function (item, includeMatches, ignoreOnlyDateModified) {
+Zotero.Item.prototype.diff = function (item, includeMatches, ignoreFields) {
var diff = [];
+
+ if (!ignoreFields) {
+ ignoreFields = [];
+ }
+
var thisData = this.serialize();
var otherData = item.serialize();
@@ -3110,12 +3318,23 @@ Zotero.Item.prototype.diff = function (item, includeMatches, ignoreOnlyDateModif
//Zotero.debug(otherData);
//Zotero.debug(diff);
- // DEBUG: ignoreOnlyDateModified wouldn't work if includeMatches was set?
- if (numDiffs == 0 ||
- (ignoreOnlyDateModified && numDiffs == 1
- && diff[0].primary && diff[0].primary.dateModified)) {
+ if (numDiffs == 0) {
return false;
}
+ if (ignoreFields.length && diff[0].primary) {
+ if (includeMatches) {
+ throw ("ignoreFields cannot be used if includeMatches is set");
+ }
+ var realDiffs = numDiffs;
+ for each(var field in ignoreFields) {
+ if (diff[0].primary[field] != undefined) {
+ realDiffs--;
+ if (realDiffs == 0) {
+ return false;
+ }
+ }
+ }
+ }
return diff;
}
@@ -3124,31 +3343,58 @@ Zotero.Item.prototype.diff = function (item, includeMatches, ignoreOnlyDateModif
/**
* Returns an unsaved copy of the item
*/
-Zotero.Item.prototype.clone = function(includePrimary) {
+Zotero.Item.prototype.clone = function(includePrimary, newItem) {
Zotero.debug('Cloning item ' + this.id);
+ if (includePrimary && newItem) {
+ throw ("includePrimary and newItem parameters are mutually exclusive in Zotero.Item.clone()");
+ }
+
Zotero.DB.beginTransaction();
var obj = this.serialize();
var itemTypeID = this.itemTypeID;
- var newItem = new Zotero.Item(includePrimary ? this.id : false, itemTypeID);
- if (includePrimary) {
- for (var field in obj.primary) {
- switch (field) {
- case 'itemID':
- case 'itemType':
- continue;
+ if (newItem) {
+ var sameLibrary = newItem.libraryID == this.libraryID;
+ }
+ else {
+ var newItem = new Zotero.Item(itemTypeID);
+ var sameLibrary = true;
+
+ if (includePrimary) {
+ newItem.id = this.id;
+ newItem.libraryID = this.libraryID;
+ newItem.key = this.key;
+ for (var field in obj.primary) {
+ switch (field) {
+ case 'itemID':
+ case 'itemType':
+ case 'libraryID':
+ case 'key':
+ continue;
+ }
+ newItem.setField(field, obj.primary[field]);
}
- newItem.setField(field, obj.primary[field]);
}
}
+ var changedFields = {};
for (var field in obj.fields) {
var fieldID = Zotero.ItemFields.getID(field);
if (fieldID && Zotero.ItemFields.isValidForType(fieldID, itemTypeID)) {
newItem.setField(field, obj.fields[field]);
+ changedFields[field] = true;
+ }
+ }
+ // If modifying an existing item, clear other fields not in the cloned item
+ if (newItem) {
+ var previousFields = this.getUsedFields(true);
+ for each(var field in previousFields) {
+ if (!changedFields[field]) {
+ newItem.setField(field, false);
+ }
}
}
@@ -3178,42 +3424,78 @@ Zotero.Item.prototype.clone = function(includePrimary) {
}
}
else {
+ // If overwriting an existing item, clear existing creators
+ if (newItem) {
+ for (var i=newItem.numCreators()-1; i>=0; i--) {
+ if (newItem.getCreator(i)) {
+ newItem.removeCreator(i);
+ }
+ }
+ }
+
var i = 0;
for (var c in obj.creators) {
- newItem.setCreator(
- i, this.getCreator(c).ref, this.getCreator(c).creatorTypeID
- );
+ var creator = this.getCreator(c).ref;
+ var creatorTypeID = this.getCreator(c).creatorTypeID;
+
+ if (!sameLibrary) {
+ var creatorDataID = Zotero.Creators.getDataID(this.getCreator(c).ref);
+ var creatorIDs = Zotero.Creators.getCreatorsWithData(creatorDataID, newItem.libraryID);
+ if (creatorIDs) {
+ // TODO: support multiple creators?
+ var creator = Zotero.Creators.get(creatorIDs[0]);
+ }
+ else {
+ var newCreator = new Zotero.Creator;
+ newCreator.libraryID = newItem.libraryID;
+ newCreator.setFields(creator);
+ var creator = newCreator;
+ }
+
+ var creatorTypeID = this.getCreator(c).creatorTypeID;
+ }
+
+ newItem.setCreator(i, creator, creatorTypeID);
i++;
}
}
}
else {
newItem.setNote(this.getNote());
- var parent = this.getSource();
- if (parent) {
- newItem.setSource(parent);
+ if (sameLibrary) {
+ var parent = this.getSource();
+ if (parent) {
+ newItem.setSource(parent);
+ }
}
if (this.isAttachment()) {
newItem.attachmentLinkMode = this.attachmentLinkMode;
newItem.attachmentMIMEType = this.attachmentMIMEType;
newItem.attachmentCharset = this.attachmentCharset;
- if (this.attachmentPath) {
- newItem.attachmentPath = this.attachmentPath;
- }
- if (this.attachmentSyncState) {
- newItem.attachmentSyncState = this.attachmentSyncState;
+ if (sameLibrary) {
+ if (this.attachmentPath) {
+ newItem.attachmentPath = this.attachmentPath;
+ }
+ if (this.attachmentSyncState) {
+ newItem.attachmentSyncState = this.attachmentSyncState;
+ }
}
}
}
if (obj.tags) {
for each(var tag in obj.tags) {
- newItem.addTagByID(tag.primary.tagID);
+ if (sameLibrary) {
+ newItem.addTagByID(tag.primary.tagID);
+ }
+ else {
+ newItem.addTag(tag.fields.name, tag.fields.type);
+ }
}
}
- if (obj.related) {
+ if (obj.related && sameLibrary) {
// DEBUG: this will add reverse-only relateds too
newItem.relatedItems = obj.related;
}
@@ -3246,6 +3528,12 @@ Zotero.Item.prototype.erase = function(deleteChildren) {
var deletedItemNotifierData = {};
deletedItemNotifierData[this.id] = { old: this.serialize() };
+ // Remove group item metadata
+ if (this.libraryID) {
+ var sql = "DELETE FROM groupItems WHERE itemID=?";
+ Zotero.DB.query(sql, this.id);
+ }
+
// Remove item from parent collections
var parentCollectionIDs = this.getCollections();
if (parentCollectionIDs) {
@@ -3308,7 +3596,7 @@ Zotero.Item.prototype.erase = function(deleteChildren) {
if (toDelete) {
for (var i in toDelete) {
var obj = Zotero.Items.get(toDelete[i]);
- obj.erase(true);
+ obj.erase();
}
}
}
@@ -3376,7 +3664,20 @@ Zotero.Item.prototype.erase = function(deleteChildren) {
Zotero.DB.query('DELETE FROM itemAttachments WHERE itemID=?', this.id);
Zotero.DB.query('DELETE FROM itemSeeAlso WHERE itemID=?', this.id);
Zotero.DB.query('DELETE FROM itemSeeAlso WHERE linkedItemID=?', this.id);
- Zotero.DB.query('DELETE FROM itemTags WHERE itemID=?', this.id);
+
+ var tags = this.getTags();
+ if (tags) {
+ var hasTags = true;
+ Zotero.DB.query('DELETE FROM itemTags WHERE itemID=?', this.id);
+ // DEBUG: Hack to reload linked items -- replace with something better
+ for each(var tag in tags) {
+ tag._linkedItemsLoaded = false;
+ }
+ }
+ else {
+ var hasTags = false;
+ }
+
Zotero.DB.query('DELETE FROM itemData WHERE itemID=?', this.id);
Zotero.DB.query('DELETE FROM items WHERE itemID=?', this.id);
@@ -3410,6 +3711,9 @@ Zotero.Item.prototype.erase = function(deleteChildren) {
if (hasCreators) {
Zotero.Prefs.set('purge.creators', true);
}
+ if (hasTags) {
+ Zotero.Prefs.set('purge.tags', true);
+ }
}
@@ -3421,7 +3725,7 @@ Zotero.Item.prototype.isCollection = function() {
Zotero.Item.prototype.toArray = function (mode) {
Zotero.debug('Zotero.Item.toArray() is deprecated -- use Zotero.Item.serialize()');
- if (this.id) {
+ if (this.id || this.key) {
if (!this._primaryDataLoaded) {
this.loadPrimaryData(true);
}
@@ -3433,8 +3737,12 @@ Zotero.Item.prototype.toArray = function (mode) {
var arr = {};
// Primary fields
- for (var i in Zotero.Item.primaryFields) {
+ for each(var i in Zotero.Items.primaryFields) {
switch (i) {
+ case 'itemID':
+ arr.itemID = this._id;
+ continue;
+
case 'itemTypeID':
arr.itemType = Zotero.ItemTypes.getName(this.itemTypeID);
continue;
@@ -3550,7 +3858,7 @@ Zotero.Item.prototype.toArray = function (mode) {
* 2 == e.g. [Stothard; Letter to Valee; May 8, 1928]
*/
Zotero.Item.prototype.serialize = function(mode) {
- if (this.id) {
+ if (this.id || this.key) {
if (!this._primaryDataLoaded) {
this.loadPrimaryData(true);
}
@@ -3565,8 +3873,12 @@ Zotero.Item.prototype.serialize = function(mode) {
arr.fields = {};
// Primary and virtual fields
- for (var i in Zotero.Item.primaryFields) {
+ for each(var i in Zotero.Items.primaryFields) {
switch (i) {
+ case 'itemID':
+ arr.primary.itemID = this._id;
+ continue;
+
case 'itemTypeID':
arr.primary.itemType = Zotero.ItemTypes.getName(this.itemTypeID);
continue;
@@ -3611,12 +3923,13 @@ Zotero.Item.prototype.serialize = function(mode) {
for (var i in creators) {
var creator = {};
// Convert creatorTypeIDs to text
- creator.creatorType =
- Zotero.CreatorTypes.getName(creators[i].creatorTypeID);
+ creator.creatorType = Zotero.CreatorTypes.getName(creators[i].creatorTypeID);
creator.creatorID = creators[i].ref.id;
creator.firstName = creators[i].ref.firstName;
creator.lastName = creators[i].ref.lastName;
creator.fieldMode = creators[i].ref.fieldMode;
+ creator.libraryID = creators[i].ref.libraryID;
+ creator.key = creators[i].ref.key;
arr.creators.push(creator);
}
@@ -3702,7 +4015,8 @@ Zotero.Item.prototype._loadCreators = function() {
for (var i=0; i<creators.length; i++) {
var creatorObj = Zotero.Creators.get(creators[i].creatorID);
if (!creatorObj) {
- creatorObj = new Zotero.Creator(creators[i].creatorID);
+ creatorObj = new Zotero.Creator();
+ creatorObj.id = creators[i].creatorID;
}
this._creators[creators[i].orderIndex] = {
ref: creatorObj,
@@ -3747,7 +4061,7 @@ Zotero.Item.prototype._loadRelatedItems = function() {
if (!this.id) {
return;
}
-
+
if (!this._primaryDataLoaded) {
this.loadPrimaryData(true);
}
diff --git a/chrome/content/zotero/xpcom/data/items.js b/chrome/content/zotero/xpcom/data/items.js
@@ -38,9 +38,29 @@ Zotero.Items = new function() {
this.getFirstCreatorSQL = getFirstCreatorSQL;
this.getSortTitle = getSortTitle;
+ this.__defineGetter__('primaryFields', function () {
+ if (!_primaryFields.length) {
+ _primaryFields = Zotero.DB.getColumns('items');
+ _primaryFields.splice(_primaryFields.indexOf('clientDateModified'), 1);
+ _primaryFields = _primaryFields.concat(
+ ['firstCreator', 'numNotes', 'numAttachments']
+ );
+ }
+
+ // Make a copy of array
+ var fields = [];
+ for each(var field in _primaryFields) {
+ fields.push(field);
+ }
+ return fields;
+ });
+
+ this.__defineGetter__('linkedItemPredicate', function () "owl:sameAs");
+
// Private members
var _cachedFields = [];
var _firstCreatorSQL = '';
+ var _primaryFields = [];
/*
@@ -173,7 +193,7 @@ Zotero.Items = new function() {
* var item = Zotero.Items.add('book', data);
*/
function add(itemTypeOrID, data) {
- var item = new Zotero.Item(false, itemTypeOrID);
+ var item = new Zotero.Item(itemTypeOrID);
for (var field in data) {
if (field == 'creators') {
var i = 0;
@@ -212,6 +232,11 @@ Zotero.Items = new function() {
}
+ this.isPrimaryField = function (field) {
+ return this.primaryFields.indexOf(field) != -1;
+ }
+
+
function cacheFields(fields, items) {
if (items && items.length == 0) {
return;
@@ -236,7 +261,7 @@ Zotero.Items = new function() {
_cachedFields.push(field);
- if (Zotero.Item.prototype.isPrimaryField(field)) {
+ if (this.isPrimaryField(field)) {
primaryFields.push(field);
}
else {
diff --git a/chrome/content/zotero/xpcom/data/libraries.js b/chrome/content/zotero/xpcom/data/libraries.js
@@ -0,0 +1,44 @@
+Zotero.Libraries = new function () {
+ this.exists = function (libraryID) {
+ var sql = "SELECT COUNT(*) FROM libraries WHERE libraryID=?";
+ return !!Zotero.DB.valueQuery(sql, [libraryID]);
+ }
+
+
+ this.add = function (libraryID, type) {
+ switch (type) {
+ case 'group':
+ break;
+
+ default:
+ throw ("Invalid library type '" + type + "' in Zotero.Libraries.add()");
+ }
+
+ var sql = "INSERT INTO libraries (libraryID, libraryType) VALUES (?, ?)";
+ Zotero.DB.query(sql, [libraryID, type]);
+ }
+
+
+ this.getName = function (libraryID) {
+ var type = this.getType(libraryID);
+ switch (type) {
+ case 'group':
+ var groupID = Zotero.Groups.getGroupIDFromLibraryID(libraryID);
+ var group = Zotero.Groups.get(groupID);
+ return group.name;
+
+ default:
+ throw ("Unsupported library type '" + type + "' in Zotero.Libraries.getName()");
+ }
+ }
+
+
+ this.getType = function (libraryID) {
+ var sql = "SELECT libraryType FROM libraries WHERE libraryID=?";
+ var libraryType = Zotero.DB.valueQuery(sql, libraryID);
+ if (!libraryType) {
+ throw ("Library " + libraryID + " does not exist in Zotero.Libraries.getType()");
+ }
+ return libraryType;
+ }
+}
diff --git a/chrome/content/zotero/xpcom/data/relation.js b/chrome/content/zotero/xpcom/data/relation.js
@@ -0,0 +1,149 @@
+Zotero.Relation = function () {
+ this._id = null;
+ this._libraryID = null;
+ this._subject = null;
+ this._predicate = null;
+ this._object = null;
+ this._clientDateModified = null;
+
+ this._loaded = false;
+}
+
+Zotero.Relation.prototype.__defineGetter__('objectType', function () 'relation');
+Zotero.Relation.prototype.__defineGetter__('id', function () this._id);
+Zotero.Relation.prototype.__defineSetter__('id', function (val) { this._set('id', val); });
+Zotero.Relation.prototype.__defineGetter__('libraryID', function () this._get('libraryID'));
+Zotero.Relation.prototype.__defineSetter__('libraryID', function (val) { return this._set('libraryID', val); });
+Zotero.Relation.prototype.__defineGetter__('key', function () this._id);
+//Zotero.Relation.prototype.__defineSetter__('key', function (val) { this._set('key', val) });
+Zotero.Relation.prototype.__defineGetter__('dateModified', function () this._get('dateModified'));
+Zotero.Relation.prototype.__defineGetter__('subject', function () this._get('subject'));
+Zotero.Relation.prototype.__defineSetter__('subject', function (val) { this._set('subject', val); });
+Zotero.Relation.prototype.__defineGetter__('predicate', function () this._get('predicate'));
+Zotero.Relation.prototype.__defineSetter__('predicate', function (val) { this._set('predicate', val); });
+Zotero.Relation.prototype.__defineGetter__('object', function () this._get('object'));
+Zotero.Relation.prototype.__defineSetter__('object', function (val) { this._set('object', val); });
+
+
+Zotero.Relation.prototype._get = function (field) {
+ if (this._id && !this._loaded) {
+ this.load();
+ }
+ return this['_' + field];
+}
+
+
+Zotero.Relation.prototype._set = function (field, val) {
+ switch (field) {
+ case 'id':
+ case 'libraryID':
+ if (field == 'libraryID' && !val) {
+ throw ("libraryID cannot be empty in Zotero.Relation._set()");
+ }
+
+ if (val == this['_' + field]) {
+ return;
+ }
+
+ if (this._loaded) {
+ throw ("Cannot set " + field + " after object is already loaded in Zotero.Relation._set()");
+ }
+ this['_' + field] = val;
+ return;
+ }
+
+ if (this.id) {
+ if (!this._loaded) {
+ this.load();
+ }
+ }
+ else {
+ this._loaded = true;
+ }
+
+ if (this['_' + field] != val) {
+ //this._prepFieldChange(field);
+
+ switch (field) {
+ default:
+ this['_' + field] = val;
+ }
+ }
+}
+
+
+/**
+ * Check if search exists in the database
+ *
+ * @return bool TRUE if the relation exists, FALSE if not
+ */
+Zotero.Relation.prototype.exists = function () {
+ if (this.id) {
+ var sql = "SELECT COUNT(*) FROM relations WHERE relationID=?";
+ return !!Zotero_DB::valueQuery(sql, this.id);
+ }
+
+ if (this.libraryID && this.subject && this.predicate && this.object) {
+ var sql = "SELECT COUNT(*) FROM relations WHERE libraryID=? AND "
+ + "subject=? AND predicate=? AND object=?";
+ var params = [this.libraryID, this.subject, this.predicate, this.object];
+ return !!Zotero.DB.valueQuery(sql, params);
+ }
+
+ throw ("ID or libraryID/subject/predicate/object not set in Zotero.Relation.exists()");
+}
+
+
+
+Zotero.Relation.prototype.load = function () {
+ var id = this._id;
+ if (!id) {
+ throw ("ID not set in Zotero.Relation.load()");
+ }
+
+ var sql = "SELECT * FROM relations WHERE ROWID=?";
+ var row = Zotero.DB.rowQuery(sql, id);
+ if (!row) {
+ return;
+ }
+
+ this._libraryID = row.libraryID;
+ this._subject = row.subject;
+ this._predicate = row.predicate;
+ this._object = row.object;
+ this._clientDateModified = row.clientDateModified;
+ this._loaded = true;
+
+ return true;
+}
+
+
+Zotero.Relation.prototype.save = function () {
+ if (this.id) {
+ throw ("Existing relations cannot currently be altered in Zotero.Relation.save()");
+ }
+
+ if (!this.subject) {
+ throw ("Missing subject in Zotero.Relation.save()");
+ }
+ if (!this.predicate) {
+ throw ("Missing predicate in Zotero.Relation.save()");
+ }
+ if (!this.object) {
+ throw ("Missing object in Zotero.Relation.save()");
+ }
+
+ var sql = "INSERT INTO relations (libraryID, subject, predicate, object) VALUES (?, ?, ?, ?)";
+ var insertID = Zotero.DB.query(sql, [this.libraryID, this.subject, this.predicate, this.object]);
+
+ return insertID;
+}
+
+
+Zotero.Relation.prototype.toXML = function () {
+ var xml = <relation/>;
+ xml.subject = this.subject;
+ xml.predicate = this.predicate;
+ xml.object = this.object;
+ return xml;
+}
diff --git a/chrome/content/zotero/xpcom/data/relations.js b/chrome/content/zotero/xpcom/data/relations.js
@@ -0,0 +1,149 @@
+Zotero.Relations = new function () {
+ Zotero.DataObjects.apply(this, ['relation']);
+ this.constructor.prototype = new Zotero.DataObjects();
+
+ var _namespaces = {
+ owl: 'http://www.w3.org/2002/07/owl#'
+ };
+
+
+ this.get = function (id) {
+ if (typeof id != 'number') {
+ throw ("id '" + id + "' must be an integer in Zotero.Relations.get()");
+ }
+
+ var relation = new Zotero.Relation;
+ relation.id = id;
+ return relation;
+ }
+
+
+ /**
+ * @return {Object[]}
+ */
+ this.getByURIs = function (subject, predicate, object) {
+ predicate = _getPrefixAndValue(predicate).join(':');
+ if (!subject && !predicate && !object) {
+ throw ("No values provided in Zotero.Relations.get()");
+ }
+
+ var sql = "SELECT ROWID FROM relations WHERE 1";
+ var params = [];
+ if (subject) {
+ sql += " AND subject=?";
+ params.push(subject);
+ }
+ if (predicate) {
+ sql += " AND predicate=?";
+ params.push(predicate);
+ }
+ if (object) {
+ sql += " AND object=?";
+ params.push(object);
+ }
+ var rows = Zotero.DB.columnQuery(sql, params);
+ if (!rows) {
+ return [];
+ }
+
+ var toReturn = [];
+ for each(var id in rows) {
+ var relation = new Zotero.Relation;
+ relation.id = id;
+ toReturn.push(relation);
+ }
+ return toReturn;
+ }
+
+
+ this.getSubject = function (subject, predicate, object) {
+ var subjects = [];
+ var relations = this.getByURIs(subject, predicate, object);
+ for each(var relation in relations) {
+ subjects.push(relation.subject);
+ }
+ return subjects;
+ }
+
+
+ this.getObject = function (subject, predicate, object) {
+ var objects = [];
+ var relations = this.getByURIs(subject, predicate, object);
+ for each(var relation in relations) {
+ objects.push(relation.object);
+ }
+ return objects;
+ }
+
+
+ this.add = function (libraryID, subject, predicate, object) {
+ predicate = _getPrefixAndValue(predicate).join(':');
+
+ var relation = new Zotero.Relation;
+ if (libraryID) {
+ relation.libraryID = parseInt(libraryID);
+ }
+ else {
+ libraryID = Zotero.libraryID;
+ if (!libraryID) {
+ libraryID = Zotero.getLocalUserKey(true);
+ }
+ relation.libraryID = parseInt(libraryID);
+ }
+ relation.subject = subject;
+ relation.predicate = predicate;
+ relation.object = object;
+ relation.save();
+ }
+
+
+ this.erase = function (id) {
+ Zotero.DB.beginTransaction();
+
+ var sql = "DELETE FROM relations WHERE ROWID=?";
+ Zotero.DB.query(sql, [id]);
+
+ // TODO: log to syncDeleteLog
+
+ Zotero.DB.commitTransaction();
+ }
+
+
+ this.xmlToRelation = function (xml) {
+ var relation = new Zotero.Relation;
+ var libraryID = xml.@libraryID.toString();
+ if (libraryID) {
+ relation.libraryID = parseInt(libraryID);
+ }
+ else {
+ libraryID = Zotero.libraryID;
+ if (!libraryID) {
+ libraryID = Zotero.getLocalUserKey(true);
+ }
+ relation.libraryID = parseInt(libraryID);
+ }
+ relation.subject = xml.subject.toString();
+ relation.predicate = xml.predicate.toString();
+ relation.object = xml.object.toString();
+ return relation;
+ }
+
+
+ function _getPrefixAndValue(uri) {
+ var [prefix, value] = uri.split(':');
+ if (prefix && value) {
+ if (!_namespaces[prefix]) {
+ throw ("Invalid prefix '" + prefix + "' in Zotero.Relations.add()");
+ }
+ return [prefix, value];
+ }
+
+ for (var prefix in namespaces) {
+ if (uri.indexOf(namespaces[prefix]) == 0) {
+ var value = uri.substr(namespaces[prefix].length - 1)
+ return [prefix, value];
+ }
+ }
+ throw ("Invalid namespace in URI '" + uri + "' in Zotero.Relations._getPrefixAndValue()");
+ }
+}
diff --git a/chrome/content/zotero/xpcom/data/tag.js b/chrome/content/zotero/xpcom/data/tag.js
@@ -21,18 +21,23 @@
*/
-Zotero.Tag = function(tagID) {
- this._tagID = tagID ? tagID : null;
+Zotero.Tag = function () {
+ if (arguments[0]) {
+ throw ("Zotero.Tag constructor doesn't take any parameters");
+ }
+
this._init();
}
Zotero.Tag.prototype._init = function () {
// Public members for access by public methods -- do not access directly
+ this._id = null;
+ this._libraryID = null
+ this._key = null;
this._name = null;
this._type = null;
this._dateAdded = null;
this._dateModified = null;
- this._key = null;
this._loaded = false;
this._changed = false;
@@ -43,9 +48,13 @@ Zotero.Tag.prototype._init = function () {
}
-Zotero.Tag.prototype.__defineGetter__('id', function () { return this._tagID; });
-
-Zotero.Tag.prototype.__defineSetter__('tagID', function (val) { this._set('tagID', val); });
+Zotero.Tag.prototype.__defineGetter__('objectType', function () { return 'tag'; });
+Zotero.Tag.prototype.__defineGetter__('id', function () { return this._get('id'); });
+Zotero.Tag.prototype.__defineSetter__('id', function (val) { this._set('id', val); });
+Zotero.Tag.prototype.__defineGetter__('libraryID', function () { return this._get('libraryID'); });
+Zotero.Tag.prototype.__defineSetter__('libraryID', function (val) { return this._set('libraryID', val); });
+Zotero.Tag.prototype.__defineGetter__('key', function () { return this._get('key'); });
+Zotero.Tag.prototype.__defineSetter__('key', function (val) { this._set('key', val) });
Zotero.Tag.prototype.__defineGetter__('name', function () { return this._get('name'); });
Zotero.Tag.prototype.__defineSetter__('name', function (val) { this._set('name', val); });
Zotero.Tag.prototype.__defineGetter__('type', function () { return this._get('type'); });
@@ -55,13 +64,12 @@ Zotero.Tag.prototype.__defineSetter__('dateAdded', function (val) { this._set('d
Zotero.Tag.prototype.__defineGetter__('dateModified', function () { return this._get('dateModified'); });
Zotero.Tag.prototype.__defineSetter__('dateModified', function (val) { this._set('dateModified', val); });
Zotero.Tag.prototype.__defineGetter__('key', function () { return this._get('key'); });
-Zotero.Tag.prototype.__defineSetter__('key', function (val) { this._set('key', val); });
Zotero.Tag.prototype.__defineSetter__('linkedItems', function (arr) { this._setLinkedItems(arr); });
Zotero.Tag.prototype._get = function (field) {
- if (this.id && !this._loaded) {
+ if ((this._id || this._key) && !this._loaded) {
this.load();
}
return this['_' + field];
@@ -69,17 +77,27 @@ Zotero.Tag.prototype._get = function (field) {
Zotero.Tag.prototype._set = function (field, val) {
- if (field == 'name') {
- val = Zotero.Utilities.prototype.trim(val);
- }
-
switch (field) {
- case 'id': // set using constructor
- //case 'tagID': // set using constructor
- throw ("Invalid field '" + field + "' in Zotero.Tag.set()");
+ case 'id':
+ case 'libraryID':
+ case 'key':
+ if (val == this['_' + field]) {
+ return;
+ }
+
+ if (this._loaded) {
+ throw ("Cannot set " + field + " after object is already loaded in Zotero.Tag._set()");
+ }
+ //this._checkValue(field, val);
+ this['_' + field] = val;
+ return;
+
+ case 'name':
+ val = Zotero.Utilities.prototype.trim(val);
+ break;
}
- if (this.id) {
+ if (this.id || this.key) {
if (!this._loaded) {
this.load();
}
@@ -118,14 +136,38 @@ Zotero.Tag.prototype.exists = function() {
* Build tag from database
*/
Zotero.Tag.prototype.load = function() {
- Zotero.debug("Loading data for tag " + this.id + " in Zotero.Tag.load()");
+ var id = this._id;
+ var key = this._key;
+ var libraryID = this._libraryID;
+ var desc = id ? id : libraryID + "/" + key;
- if (!this.id) {
- throw ("tagID not set in Zotero.Tag.load()");
+ Zotero.debug("Loading data for tag " + desc + " in Zotero.Tag.load()");
+
+ if (!id && !key) {
+ throw ("ID or key not set in Zotero.Tag.load()");
+ }
+
+ var sql = "SELECT * FROM tags WHERE ";
+ if (id) {
+ sql += "tagID=?";
+ var params = id;
+ }
+ else {
+ sql += "key=?";
+ var params = [key];
+ if (libraryID) {
+ sql += " AND libraryID=?";
+ params.push(libraryID);
+ }
+ else {
+ sql += " AND libraryID IS NULL";
+ }
}
+ var row = Zotero.DB.rowQuery(sql, params);
- var sql = "SELECT name, type, dateAdded, dateModified, key FROM tags WHERE tagID=?";
- var row = Zotero.DB.rowQuery(sql, this.id);
+ if (!row) {
+ return;
+ }
this.loadFromRow(row);
return true;
@@ -136,6 +178,18 @@ Zotero.Tag.prototype.loadFromRow = function (row) {
this._init();
for (var col in row) {
//Zotero.debug("Setting field '" + col + "' to '" + row[col] + "' for tag " + this.id);
+ switch (col) {
+ case 'clientDateModified':
+ continue;
+
+ case 'tagID':
+ this._id = row[col];
+ continue;
+
+ case 'libraryID':
+ this['_' + col] = row[col] ? row[col] : null;
+ continue;
+ }
this['_' + col] = (!row[col] && row[col] !== 0) ? '' : row[col];
}
this._loaded = true;
@@ -220,12 +274,15 @@ Zotero.Tag.prototype.removeItem = function (itemID) {
Zotero.Tag.prototype.save = function (full) {
+ Zotero.Tags.editCheck(this);
+
// Default to manual tag
if (!this.type) {
this.type = 0;
}
if (this.type != 0 && this.type != 1) {
+ Zotero.debug(this);
throw ('Invalid tag type ' + this.type + ' for tag ' + this.id + ' in Zotero.Tag.save()');
}
@@ -241,34 +298,6 @@ Zotero.Tag.prototype.save = function (full) {
Zotero.DB.beginTransaction();
- // ID change
- if (this._changed.tagID) {
- var oldID = this._previousData.primary.tagID;
- var params = [this.id, oldID];
-
- Zotero.debug("Changing tagID " + oldID + " to " + this.id);
-
- var row = Zotero.DB.rowQuery("SELECT * FROM tags WHERE tagID=?", oldID);
-
- // Set type on old row to -1, since there's a UNIQUE on name/type
- Zotero.DB.query("UPDATE tags SET type=-1 WHERE tagID=?", oldID);
-
- // Add a new row so we can update the old rows despite FK checks
- // Use temp key due to UNIQUE constraint on key column
- Zotero.DB.query("INSERT INTO tags VALUES (?, ?, ?, ?, ?, ?)",
- [this.id, row.name, row.type, row.dateAdded, row.dateModified, 'TEMPKEY']);
-
- Zotero.DB.query("UPDATE itemTags SET tagID=? WHERE tagID=?", params);
-
- Zotero.DB.query("DELETE FROM tags WHERE tagID=?", oldID);
-
- Zotero.DB.query("UPDATE tags SET key=? WHERE tagID=?", [row.key, this.id]);
-
- Zotero.Notifier.trigger('id-change', 'tag', oldID + '-' + this.id);
-
- // update caches
- }
-
var isNew = !this.id || !this.exists();
try {
@@ -281,9 +310,16 @@ Zotero.Tag.prototype.save = function (full) {
var key = this.key ? this.key : this._generateKey();
var columns = [
- 'tagID', 'name', 'type', 'dateAdded', 'dateModified', 'key'
+ 'tagID',
+ 'name',
+ 'type',
+ 'dateAdded',
+ 'dateModified',
+ 'clientDateModified',
+ 'libraryID',
+ 'key'
];
- var placeholders = ['?', '?', '?', '?', '?', '?'];
+ var placeholders = ['?', '?', '?', '?', '?', '?', '?', '?'];
var sqlValues = [
tagID ? { int: tagID } : null,
{ string: this.name },
@@ -293,6 +329,8 @@ Zotero.Tag.prototype.save = function (full) {
// If date modified hasn't changed, use current timestamp
this._changed.dateModified ?
this.dateModified : Zotero.DB.transactionDateTime,
+ Zotero.DB.transactionDateTime,
+ this.libraryID ? this.libraryID : null,
key
];
@@ -372,6 +410,8 @@ Zotero.Tag.prototype.save = function (full) {
insertStatement.execute();
}
catch (e) {
+ Zotero.debug("itemID: " + itemID);
+ Zotero.debug("tagID: " + tagID);
throw (e + ' [ERROR: ' + Zotero.DB.getLastErrorString() + ']');
}
}
@@ -392,7 +432,7 @@ Zotero.Tag.prototype.save = function (full) {
// If successful, set values in object
if (!this.id) {
- this._tagID = tagID;
+ this._id = tagID;
}
if (!this.key) {
@@ -465,9 +505,10 @@ Zotero.Tag.prototype.serialize = function () {
var obj = {
primary: {
tagID: this.id,
+ libraryID: this.libraryID,
+ key: this.key,
dateAdded: this.dateAdded,
- dateModified: this.dateModified,
- key: this.key
+ dateModified: this.dateModified
},
fields: {
name: this.name,
@@ -545,10 +586,20 @@ Zotero.Tag.prototype.erase = function () {
Zotero.Tag.prototype._loadLinkedItems = function() {
+ if (!this.id && !this.key) {
+ this._linkedItemsLoaded = true;
+ return;
+ }
+
if (!this._loaded) {
this.load();
}
+ if (!this.id) {
+ this._linkedItemsLoaded = true;
+ return;
+ }
+
var sql = "SELECT itemID FROM itemTags WHERE tagID=?";
var ids = Zotero.DB.columnQuery(sql, this.id);
diff --git a/chrome/content/zotero/xpcom/data/tags.js b/chrome/content/zotero/xpcom/data/tags.js
@@ -73,25 +73,39 @@ Zotero.Tags = new function() {
/*
* Returns the tagID matching given tag and type
*/
- function getID(name, type) {
+ function getID(name, type, libraryID) {
name = Zotero.Utilities.prototype.trim(name);
var lcname = name.toLowerCase();
- if (_tags[type] && _tags[type]['_' + lcname]) {
- return _tags[type]['_' + lcname];
+ if (!libraryID) {
+ libraryID = 0;
+ }
+
+ if (_tags[libraryID] && _tags[libraryID][type] && _tags[libraryID][type]['_' + lcname]) {
+ return _tags[libraryID][type]['_' + lcname];
}
// FIXME: COLLATE NOCASE doesn't work for Unicode characters, so this
// won't find Äbc if "äbc" is entered and will allow a duplicate tag
// to be created
- var sql = 'SELECT tagID FROM tags WHERE name=? AND type=?';
- var tagID = Zotero.DB.valueQuery(sql, [name, type]);
-
+ var sql = "SELECT tagID FROM tags WHERE name=? AND type=? AND libraryID";
+ var params = [name, type];
+ if (libraryID) {
+ sql += "=?";
+ params.push(libraryID);
+ }
+ else {
+ sql += " IS NULL";
+ }
+ var tagID = Zotero.DB.valueQuery(sql, params);
if (tagID) {
- if (!_tags[type]) {
- _tags[type] = [];
+ if (!_tags[libraryID]) {
+ _tags[libraryID] = {};
+ }
+ if (!_tags[libraryID][type]) {
+ _tags[libraryID][type] = [];
}
- _tags[type]['_' + lcname] = tagID;
+ _tags[libraryID][type]['_' + lcname] = tagID;
}
return tagID;
@@ -101,20 +115,36 @@ Zotero.Tags = new function() {
/*
* Returns all tagIDs for this tag (of all types)
*/
- function getIDs(name) {
+ function getIDs(name, libraryID) {
name = Zotero.Utilities.prototype.trim(name);
- var sql = 'SELECT tagID FROM tags WHERE name=?';
- return Zotero.DB.columnQuery(sql, [name]);
+ var sql = "SELECT tagID FROM tags WHERE name=? AND libraryID";
+ var params = [name];
+ if (libraryID) {
+ sql += "=?";
+ params.push(libraryID);
+ }
+ else {
+ sql += " IS NULL";
+ }
+ return Zotero.DB.columnQuery(sql, params);
}
/*
* Returns an array of tag types for tags matching given tag
*/
- function getTypes(name) {
+ function getTypes(name, libraryID) {
name = Zotero.Utilities.prototype.trim(name);
- var sql = 'SELECT type FROM tags WHERE name=?';
- return Zotero.DB.columnQuery(sql, [name]);
+ var sql = "SELECT type FROM tags WHERE name=? AND libraryID";
+ var params = [name];
+ if (libraryID) {
+ sql += "=?";
+ params.push(libraryID);
+ }
+ else {
+ sql += " IS NULL";
+ }
+ return Zotero.DB.columnQuery(sql, params);
}
@@ -123,12 +153,25 @@ Zotero.Tags = new function() {
*
* _types_ is an optional array of tag types to fetch
*/
- function getAll(types) {
- var sql = "SELECT tagID, name FROM tags ";
+ function getAll(types, libraryID) {
+ var sql = "SELECT tagID, name FROM tags WHERE libraryID";
+ var params = [];
+ if (libraryID) {
+ sql += "=?";
+ params.push(libraryID);
+ }
+ else {
+ sql += " IS NULL";
+ }
if (types) {
- sql += "WHERE type IN (" + types.join() + ") ";
+ sql += " AND type IN (" + types.join() + ")";
+ }
+ if (params.length) {
+ var tags = Zotero.DB.query(sql, params);
+ }
+ else {
+ var tags = Zotero.DB.query(sql);
}
- var tags = Zotero.DB.query(sql);
if (!tags) {
return {};
}
@@ -244,6 +287,7 @@ Zotero.Tags = new function() {
Zotero.DB.beginTransaction();
var tagObj = this.get(tagID);
+ var oldLibraryID = tagObj.libraryID;
var oldName = tagObj.name;
var oldType = tagObj.type;
var notifierData = {};
@@ -278,8 +322,8 @@ Zotero.Tags = new function() {
// Manual purge of old tag
sql = "DELETE FROM tags WHERE tagID=?";
Zotero.DB.query(sql, tagID);
- if (_tags[oldType]) {
- delete _tags[oldType]['_' + oldName];
+ if (_tags[oldLibraryID] && _tags[oldLibraryID][oldType]) {
+ delete _tags[oldLibraryID][oldType]['_' + oldName];
}
delete this._objectCache[tagID];
Zotero.Notifier.trigger('delete', 'tag', tagID, notifierData);
@@ -426,8 +470,9 @@ Zotero.Tags = new function() {
for each(var id in ids) {
var tag = this._objectCache[id];
delete this._objectCache[id];
- if (tag && _tags[tag.type]) {
- delete _tags[tag.type]['_' + tag.name];
+ var libraryID = tag.libraryID ? tag.libraryID : 0;
+ if (tag && _tags[libraryID] && _tags[libraryID][tag.type]) {
+ delete _tags[libraryID][tag.type]['_' + tag.name];
}
}
}
diff --git a/chrome/content/zotero/xpcom/data_access.js b/chrome/content/zotero/xpcom/data_access.js
@@ -39,6 +39,9 @@ Zotero.getCollections = function(parent, recursive) {
var sql = "SELECT collectionID AS id, collectionName AS name FROM collections C "
+ "WHERE parentCollectionID " + (parent ? '=' + parent : 'IS NULL');
+ if (!parent) {
+ sql += " AND libraryID IS NULL";
+ }
var children = Zotero.DB.query(sql);
if (!children) {
diff --git a/chrome/content/zotero/xpcom/file.js b/chrome/content/zotero/xpcom/file.js
@@ -148,6 +148,25 @@ Zotero.File = new function(){
}
+ /**
+ * @param {nsIFile} file
+ * @return {String} Base-64 representation of MD5 hash
+ */
+ this.getFileHash = function (file) {
+ var fis = Components.classes["@mozilla.org/network/file-input-stream;1"]
+ .createInstance(Components.interfaces.nsIFileInputStream);
+ fis.init(file, -1, -1, false);
+
+ var hash = Components.classes["@mozilla.org/security/hash;1"].
+ createInstance(Components.interfaces.nsICryptoHash);
+ hash.init(Components.interfaces.nsICryptoHash.MD5);
+ hash.updateFromStream(fis, 4294967295); // PR_UINT32_MAX
+ hash = hash.finish(true);
+ fis.close();
+ return hash;
+ }
+
+
/*
* Write string to a file, overwriting existing file if necessary
*/
diff --git a/chrome/content/zotero/xpcom/fulltext.js b/chrome/content/zotero/xpcom/fulltext.js
@@ -136,7 +136,7 @@ Zotero.Fulltext = new function(){
return false;
}
- versionFile = exec.parent;
+ var versionFile = exec.parent;
versionFile.append(fileName + '.version');
if (versionFile.exists()) {
var version = Zotero.File.getSample(versionFile).split(/[\r\n\s]/)[0];
diff --git a/chrome/content/zotero/xpcom/ingester.js b/chrome/content/zotero/xpcom/ingester.js
@@ -31,12 +31,14 @@ Zotero.Ingester = new function() {
getService(Components.interfaces.nsIWindowWatcher).activeWindow;
frontWindow.Zotero_Browser.progress.show();
- var saveLocation = null;
+ var libraryID = null;
+ var collection = null;
try {
- saveLocation = frontWindow.ZoteroPane.getSelectedCollection();
+ libraryID = frontWindow.ZoteroPane.getSelectedLibraryID();
+ collection = frontWindow.ZoteroPane.getSelectedCollection();
} catch(e) {}
- translation.setHandler("itemDone", function(obj, item) { frontWindow.Zotero_Browser.itemDone(obj, item, saveLocation) });
- translation.setHandler("done", function(obj, item) { frontWindow.Zotero_Browser.finishScraping(obj, item, saveLocation) });
+ translation.setHandler("itemDone", function(obj, item) { frontWindow.Zotero_Browser.itemDone(obj, item, collection) });
+ translation.setHandler("done", function(obj, item) { frontWindow.Zotero_Browser.finishScraping(obj, item, collection) });
// attempt to retrieve translators
var translators = translation.getTranslators();
@@ -48,7 +50,7 @@ Zotero.Ingester = new function() {
// translate using first available
translation.setTranslator(translators[0]);
- translation.translate();
+ translation.translate(libraryID);
}
}
diff --git a/chrome/content/zotero/xpcom/itemTreeView.js b/chrome/content/zotero/xpcom/itemTreeView.js
@@ -242,13 +242,6 @@ Zotero.ItemTreeView.prototype.refresh = function()
}
-Zotero.ItemTreeView.prototype.__defineGetter__('readOnly', function () {
- if (this._itemGroup.isTrash() || this._itemGroup.isShare()) {
- return true;
- }
- return false;
-});
-
/*
* Called by Zotero.Notifier on any changes to items in the data layer
*/
@@ -264,6 +257,8 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData)
return;
}
+ var itemGroup = this._itemGroup;
+
var madeChanges = false;
var sort = false;
@@ -272,7 +267,7 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData)
// If refreshing a single item, just unselect and reselect it
if (action == 'refresh') {
if (type == 'share-items') {
- if (this._itemGroup.isShare()) {
+ if (itemGroup.isShare()) {
this.refresh();
}
}
@@ -299,13 +294,14 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData)
var quicksearch = this._ownerDocument.getElementById('zotero-tb-search');
+
// 'collection-item' ids are in the form collectionID-itemID
if (type == 'collection-item') {
var splitIDs = [];
for each(var id in ids) {
var split = id.split('-');
// Skip if not collection or not an item in this collection
- if (!this._itemGroup.isCollection() || split[0] != this._itemGroup.ref.id) {
+ if (!itemGroup.isCollection() || split[0] != this._itemGroup.ref.id) {
continue;
}
splitIDs.push(split[1]);
@@ -319,23 +315,16 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData)
}
}
- if ((action == 'remove' && !this._itemGroup.isLibrary())
- || action == 'delete' || action == 'id-change' || action == 'trash') {
-
- // We only care about the old ids
- if (action == 'id-change') {
- for (var i=0, len=ids.length; i<len; i++) {
- ids[i] = ids[i].split('-')[0];
- }
- }
+ if ((action == 'remove' && !itemGroup.isLibrary(true))
+ || action == 'delete' || action == 'trash') {
// Since a remove involves shifting of rows, we have to do it in order,
// so sort the ids by row
var rows = [];
for(var i=0, len=ids.length; i<len; i++)
{
- if (action == 'delete' || action == 'trash' || action == 'id-change' ||
- !this._itemGroup.ref.hasItem(ids[i])) {
+ if (action == 'delete' || action == 'trash' ||
+ !itemGroup.ref.hasItem(ids[i])) {
// Row might already be gone (e.g. if this is a child and
// 'modify' was sent to parent)
if (this._itemRowMap[ids[i]] != undefined) {
@@ -365,7 +354,7 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData)
else if (action == 'modify')
{
// If trash or saved search, just re-run search
- if (this._itemGroup.isTrash() || this._itemGroup.isSearch())
+ if (itemGroup.isTrash() || itemGroup.isSearch())
{
this.refresh();
madeChanges = true;
@@ -375,9 +364,11 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData)
// If no quicksearch, process modifications manually
else if (!quicksearch || quicksearch.value == '')
{
- for(var i=0, len=ids.length; i<len; i++)
- {
- var row = this._itemRowMap[ids[i]];
+ var items = Zotero.Items.get(ids);
+ for each(var item in items) {
+ var id = item.id;
+
+ var row = this._itemRowMap[id];
// Item already exists in this view
if( row != null)
{
@@ -401,25 +392,19 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData)
else if (!this.isContainer(row) && parentIndex != -1
&& !sourceItemID)
{
- var item = Zotero.Items.get(ids[i]);
this._showItem(new Zotero.ItemTreeView.TreeRow(item, 0, false), this.rowCount);
this._treebox.rowCountChanged(this.rowCount-1, 1);
- sort = ids[i];
+ sort = id;
}
// If not moved from under one item to another
else if (!(sourceItemID && parentIndex != -1 && this._itemRowMap[sourceItemID] != parentIndex)) {
- sort = ids[i];
+ sort = id;
}
madeChanges = true;
}
- else if (this._itemGroup.isLibrary() || this._itemGroup.ref.hasItem(ids[i])) {
- var item = Zotero.Items.get(ids[i]);
- if (!item) {
- // DEBUG: this shouldn't really happen but could if a
- // modify comes in after a delete
- continue;
- }
+ else if (((itemGroup.isLibrary() || itemGroup.isGroup()) && itemGroup.ref.libraryID == item.libraryID)
+ || (itemGroup.isCollection() && item.inCollection(itemGroup.ref.id))) {
// Deleted items get a modify that we have to ignore when
// not viewing the trash
if (item.deleted) {
@@ -452,7 +437,7 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData)
else if(action == 'add')
{
// If saved search or trash, just re-run search
- if (this._itemGroup.isSearch() || this._itemGroup.isTrash()) {
+ if (itemGroup.isSearch() || itemGroup.isTrash()) {
this.refresh();
madeChanges = true;
sort = true;
@@ -463,24 +448,21 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData)
else if (quicksearch && quicksearch.value == '')
{
var items = Zotero.Items.get(ids);
- for (var i in items)
- {
+ for each(var item in items) {
// if the item belongs in this collection
- if((this._itemGroup.isLibrary() || items[i].inCollection(this._itemGroup.ref.id))
+ if ((((itemGroup.isLibrary() || itemGroup.isGroup()) && itemGroup.ref.libraryID == item.libraryID)
+ || (itemGroup.isCollection() && item.inCollection(itemGroup.ref.id)))
// if we haven't already added it to our hash map
- && this._itemRowMap[items[i].id] == null
+ && this._itemRowMap[item.id] == null
// Regular item or standalone note/attachment
- && (items[i].isRegularItem() || !items[i].getSource()))
- {
- this._showItem(new Zotero.ItemTreeView.TreeRow(items[i],0,false),this.rowCount);
+ && (item.isRegularItem() || !item.getSource())) {
+ this._showItem(new Zotero.ItemTreeView.TreeRow(item, 0, false), this.rowCount);
this._treebox.rowCountChanged(this.rowCount-1,1);
-
madeChanges = true;
}
}
-
if (madeChanges) {
- sort = (ids.length == 1) ? ids[0] : true;
+ sort = (items.length == 1) ? items[0].id : true;
}
}
// Otherwise re-run the search, which refreshes the item list
@@ -510,6 +492,7 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData)
// Reset to Info tab
this._ownerDocument.getElementById('zotero-view-tabs').selectedIndex = 0;
+
this.selectItem(ids[0]);
}
// If single item is selected and was modified
@@ -548,7 +531,7 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData)
}
// On delete, select item at previous position
- if (action == 'delete') {
+ if (action == 'delete' || action == 'remove') {
if (this._dataItems[previousRow]) {
this.selection.select(previousRow);
}
@@ -571,6 +554,10 @@ Zotero.ItemTreeView.prototype.notify = function(action, type, ids, extraData)
this.selectItem(selectItem);
}
+ if (Zotero.Sync.Server.syncInProgress) {
+ this.rememberSelection(savedSelection);
+ }
+
this.selection.selectEventsSuppressed = false;
}
@@ -1085,11 +1072,16 @@ Zotero.ItemTreeView.prototype.sort = function(itemID)
///
////////////////////////////////////////////////////////////////////////////////
+
/*
* Select an item
*/
Zotero.ItemTreeView.prototype.selectItem = function(id, expand, noRecurse)
{
+ if (Zotero.Sync.Server.syncInProgress) {
+ return;
+ }
+
// If no row map, we're probably in the process of switching collections,
// so store the item to select on the item group for later
if (!this._itemRowMap) {
@@ -1223,14 +1215,18 @@ Zotero.ItemTreeView.prototype.deleteSelection = function(eraseChildren, force)
ids.push(this._getItemAtRow(j).ref.id);
}
- // Erase item(s) from DB
- if (this._itemGroup.isLibrary() || force) {
+ var itemGroup = this._itemGroup;
+
+ if (itemGroup.isGroup() || (force && itemGroup.isWithinGroup())) {
+ Zotero.Items.erase(ids, eraseChildren);
+ }
+ else if (itemGroup.isLibrary() || force) {
Zotero.Items.trash(ids);
}
- else if (this._itemGroup.isCollection()) {
- this._itemGroup.ref.removeItems(ids);
+ else if (itemGroup.isCollection()) {
+ itemGroup.ref.removeItems(ids);
}
- else if (this._itemGroup.isTrash()) {
+ else if (itemGroup.isTrash()) {
Zotero.Items.erase(ids, eraseChildren);
}
this._treebox.endUpdateBatch();
@@ -1597,7 +1593,7 @@ Zotero.ItemTreeView.prototype.onDragStart = function (event, transferData, actio
var oldMethod = Zotero.isFx2 || Zotero.isFx30;
// Quick implementation of dragging of XML item format
- if (this.readOnly) {
+ if (this._itemGroup.isShare()) {
var items = this.getSelectedItems();
var xml = <data/>;
@@ -1985,7 +1981,7 @@ Zotero.ItemTreeView.prototype.canDrop = function(row, orient, dragData)
//Zotero.debug("Row is " + row + "; orient is " + orient);
if (row == -1 && orient == -1) {
- return true;
+ //return true;
}
if (!dragData) {
@@ -2001,6 +1997,8 @@ Zotero.ItemTreeView.prototype.canDrop = function(row, orient, dragData)
var ids = data;
}
+ var itemGroup = this._itemGroup;
+
// workaround... two different services call canDrop
// (nsDragAndDrop, and the tree) -- this is for the former,
// used when dragging between windows
@@ -2010,53 +2008,56 @@ Zotero.ItemTreeView.prototype.canDrop = function(row, orient, dragData)
if (nsDragAndDrop.mDragSession.sourceNode!=row.target)
{
if (dataType == 'zotero/item') {
+ var items = Zotero.Items.get(ids);
+
// Check if at least one item (or parent item for children) doesn't
// already exist in target
- for each(var id in ids)
- {
- var item = Zotero.Items.get(id);
-
+ for each(var item in items) {
// Skip non-top-level items
- if (!item.isRegularItem() && item.getSource())
- {
+ if (!item.isTopLevelItem()) {
continue;
}
- // DISABLED: move parent on child drag
- //var source = item.isRegularItem() ? false : item.getSource();
- //if (!this._itemGroup.ref.hasItem(source ? source : id))
- if (this._itemGroup.ref && !this._itemGroup.ref.hasItem(id))
- {
+
+ // TODO: For now, disable cross-window cross-library drag
+ if (itemGroup.ref.libraryID != item.libraryID) {
+ return false;
+ }
+
+ if (itemGroup.ref && !itemGroup.ref.hasItem(item.id)) {
return true;
}
}
}
else if (dataType == 'text/x-moz-url' || dataType == 'application/x-moz-file') {
- if (this._itemGroup.isSearch()) {
+ if (itemGroup.isSearch()) {
return false;
}
-
return true;
}
}
-
return false;
}
- // Highlight the rows correctly on drag
+ if (orient == 0) {
+ var rowItem = this._getItemAtRow(row).ref; // the item we are dragging over
+ }
- var rowItem = this._getItemAtRow(row).ref; //the item we are dragging over
if (dataType == 'zotero/item') {
+ var items = Zotero.Items.get(ids);
+
// Directly on a row
- if (orient == 0)
- {
+ if (orient == 0) {
var canDrop = false;
- for each(var id in ids) {
- var item = Zotero.Items.get(id);
-
+
+ for each(var item in items) {
// If any regular items, disallow drop
if (item.isRegularItem()) {
- canDrop = false;
- break;
+ return false;
+ }
+
+ // Disallow cross-library child drag
+ if (item.libraryID != itemGroup.ref.libraryID) {
+ return false;
}
// Only allow dragging of notes and attachments
@@ -2070,14 +2071,19 @@ Zotero.ItemTreeView.prototype.canDrop = function(row, orient, dragData)
}
// In library, allow children to be dragged out of parent
- else if (this._itemGroup.isLibrary() || this._itemGroup.isCollection())
- {
- for each(var id in ids)
- {
+ else if (itemGroup.isLibrary(true) || itemGroup.isCollection()) {
+ for each(var item in items) {
// Don't allow drag if any top-level items
- var item = Zotero.Items.get(id);
- if (item.isRegularItem() || !item.getSource())
- {
+ if (item.isTopLevelItem()) {
+ return false;
+ }
+
+ if (item.isWebAttachment()) {
+ return false;
+ }
+
+ // Disallow cross-library child drag
+ if (item.libraryID != itemGroup.ref.libraryID) {
return false;
}
}
@@ -2092,7 +2098,8 @@ Zotero.ItemTreeView.prototype.canDrop = function(row, orient, dragData)
return false;
}
}
- else if (this._itemGroup.isSearch()) {
+ // Don't allow drop into searches
+ else if (itemGroup.isSearch()) {
return false;
}
@@ -2116,18 +2123,22 @@ Zotero.ItemTreeView.prototype.drop = function(row, orient)
var dataType = dragData.dataType;
var data = dragData.data;
+ var itemGroup = this._itemGroup;
+
if (dataType == 'zotero/item') {
var ids = data;
+ var items = Zotero.Items.get(ids);
+ if (items.length < 1) {
+ return;
+ }
// Dropped directly on a row
- if (orient == 0)
- {
- // If item was a top-level item and it exists in a collection,
- // replace it in collections with the parent item
+ if (orient == 0) {
+ // Set drop target as the parent item for dragged items
+ //
+ // canDrop() limits this to child items
var rowItem = this._getItemAtRow(row).ref; // the item we are dragging over
- for each(var id in ids)
- {
- var item = Zotero.Items.get(id);
+ for each(var item in items) {
item.setSource(rowItem.id);
item.save();
}
@@ -2137,11 +2148,8 @@ Zotero.ItemTreeView.prototype.drop = function(row, orient)
else
{
// Remove from parent and make top-level
- if (this._itemGroup.isLibrary())
- {
- for each(var id in ids)
- {
- var item = Zotero.Items.get(id);
+ if (itemGroup.isLibrary(true)) {
+ for each(var item in items) {
if (!item.isRegularItem())
{
item.setSource();
@@ -2152,30 +2160,49 @@ Zotero.ItemTreeView.prototype.drop = function(row, orient)
// Add to collection
else
{
- for each(var id in ids)
+ for each(var item in items)
{
- var item = Zotero.Items.get(id);
var source = item.isRegularItem() ? false : item.getSource();
-
// Top-level item
if (source) {
item.setSource();
item.save()
}
- this._itemGroup.ref.addItem(id);
+ itemGroup.ref.addItem(id);
}
}
}
}
else if (dataType == 'text/x-moz-url' || dataType == 'application/x-moz-file') {
+ // FIXME: temporarily disable dragging in of files
+ if (dataType == 'application/x-moz-file' && itemGroup.isWithinGroup()) {
+ var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
+ .getService(Components.interfaces.nsIPromptService);
+ ps.alert(null, "", "Files cannot currently be added to group libraries.");
+ return;
+ }
+
+ // Disallow drop into read-only libraries
+ if (!itemGroup.isEditable()) {
+ var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
+ .getService(Components.interfaces.nsIWindowMediator);
+ var win = wm.getMostRecentWindow("navigator:browser");
+ win.ZoteroPane.displayCannotEditLibraryMessage();
+ return;
+ }
+
var sourceItemID = false;
var parentCollectionID = false;
+ var treerow = this._getItemAtRow(row);
if (orient == 0) {
- sourceItemID = this._getItemAtRow(row).ref.id
+ sourceItemID = treerow.ref.id
+ }
+ else if (itemGroup.isCollection()) {
+ var parentCollectionID = itemGroup.ref.id;
}
- else if (this._itemGroup.isCollection()) {
- var parentCollectionID = this._itemGroup.ref.id;
+ else if (itemGroup.isLibrary(true)) {
+ var libraryID = itemGroup.ref.libraryID;
}
var unlock = Zotero.Notifier.begin(true);
@@ -2207,7 +2234,15 @@ Zotero.ItemTreeView.prototype.drop = function(row, orient)
// Still string, so remote URL
if (typeof file == 'string') {
- Zotero.Attachments.importFromURL(url, sourceItemID, false, false, parentCollectionID);
+ if (sourceItemID) {
+ Zotero.Attachments.importFromURL(url, sourceItemID);
+ }
+ else {
+ var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
+ .getService(Components.interfaces.nsIWindowMediator);
+ var win = wm.getMostRecentWindow("navigator:browser");
+ win.ZoteroPane.addItemFromURL(url);
+ }
continue;
}
diff --git a/chrome/content/zotero/xpcom/notifier.js b/chrome/content/zotero/xpcom/notifier.js
@@ -25,7 +25,7 @@ Zotero.Notifier = new function(){
var _disabled = false;
var _types = [
'collection', 'creator', 'search', 'share', 'share-items', 'item',
- 'collection-item', 'item-tag', 'tag'
+ 'collection-item', 'item-tag', 'tag', 'group'
];
var _inTransaction;
var _locked = false;
@@ -87,7 +87,7 @@ Zotero.Notifier = new function(){
*
* event: 'add', 'modify', 'delete', 'move' ('c', for changing parent),
* 'remove' (ci, it), 'refresh', 'trash'
- * type - 'collection', 'search', 'item', 'collection-item', 'item-tag', 'tag'
+ * type - 'collection', 'search', 'item', 'collection-item', 'item-tag', 'tag', 'group'
* ids - single id or array of ids
*
* Notes:
diff --git a/chrome/content/zotero/xpcom/schema.js b/chrome/content/zotero/xpcom/schema.js
@@ -116,8 +116,8 @@ Zotero.Schema = new function(){
}
}
- var up1 = _migrateUserDataSchema(dbVersion);
var up2 = _updateSchema('system');
+ var up1 = _migrateUserDataSchema(dbVersion);
var up3 = _updateSchema('triggers');
Zotero.DB.commitTransaction();
@@ -2270,7 +2270,7 @@ Zotero.Schema = new function(){
Zotero.DB.query("DROP TABLE syncDeleteLogOld");
}
- //
+ // 1.5 Sync Preview 3.7
if (i==48) {
Zotero.DB.query("CREATE TABLE deletedItems (\n itemID INTEGER PRIMARY KEY,\n dateDeleted DEFAULT CURRENT_TIMESTAMP NOT NULL\n);");
}
@@ -2300,10 +2300,73 @@ Zotero.Schema = new function(){
Zotero.DB.query("DROP TABLE tagsOld");
}
+ // 1.5 Beta 3
if (i==50) {
Zotero.DB.query("DELETE FROM proxyHosts");
Zotero.DB.query("DELETE FROM proxies");
}
+
+ if (i==51) {
+ Zotero.DB.query("ALTER TABLE collections RENAME TO collectionsOld");
+ Zotero.DB.query("DROP INDEX creators_creatorDataID");
+ Zotero.DB.query("ALTER TABLE creators RENAME TO creatorsOld");
+ Zotero.DB.query("ALTER TABLE items RENAME TO itemsOld")
+ Zotero.DB.query("ALTER TABLE savedSearches RENAME TO savedSearchesOld");
+ Zotero.DB.query("ALTER TABLE tags RENAME TO tagsOld");
+
+ Zotero.DB.query("CREATE TABLE collections (\n collectionID INTEGER PRIMARY KEY,\n collectionName TEXT NOT NULL,\n parentCollectionID INT DEFAULT NULL,\n dateAdded TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n dateModified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n clientDateModified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n libraryID INT,\n key TEXT NOT NULL,\n UNIQUE (libraryID, key),\n FOREIGN KEY (parentCollectionID) REFERENCES collections(collectionID)\n);");
+ Zotero.DB.query("CREATE TABLE creators (\n creatorID INTEGER PRIMARY KEY,\n creatorDataID INT NOT NULL,\n dateAdded TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n dateModified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n clientDateModified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n libraryID INT,\n key TEXT NOT NULL,\n UNIQUE (libraryID, key),\n FOREIGN KEY (creatorDataID) REFERENCES creatorData(creatorDataID)\n);");
+ Zotero.DB.query("CREATE TABLE items (\n itemID INTEGER PRIMARY KEY,\n itemTypeID INT NOT NULL,\n dateAdded TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n dateModified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n clientDateModified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n libraryID INT,\n key TEXT NOT NULL,\n UNIQUE (libraryID, key)\n);");
+ Zotero.DB.query("CREATE TABLE savedSearches (\n savedSearchID INTEGER PRIMARY KEY,\n savedSearchName TEXT NOT NULL,\n dateAdded TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n dateModified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n clientDateModified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n libraryID INT,\n key TEXT NOT NULL,\n UNIQUE (libraryID, key)\n);");
+ Zotero.DB.query("CREATE TABLE tags (\n tagID INTEGER PRIMARY KEY,\n name TEXT NOT NULL COLLATE NOCASE,\n type INT NOT NULL,\n dateAdded TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n dateModified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n clientDateModified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n libraryID INT,\n key TEXT NOT NULL,\n UNIQUE (libraryID, name, type),\n UNIQUE (libraryID, key)\n);\n");
+
+ Zotero.DB.query("INSERT INTO collections SELECT collectionID, collectionName, parentCollectionID, dateAdded, dateModified, dateModified, NULL, key FROM collectionsOld");
+ Zotero.DB.query("INSERT INTO creators SELECT creatorID, creatorDataID, dateAdded, dateModified, dateModified, NULL, key FROM creatorsOld");
+ Zotero.DB.query("INSERT INTO items SELECT itemID, itemTypeID, dateAdded, dateModified, dateModified, NULL, key FROM itemsOld");
+ Zotero.DB.query("INSERT INTO savedSearches SELECT savedSearchID, savedSearchName, dateAdded, dateModified, dateModified, NULL, key FROM savedSearchesOld");
+ Zotero.DB.query("INSERT INTO tags SELECT tagID, name, type, dateAdded, dateModified, dateModified, NULL, key FROM tagsOld");
+
+ Zotero.DB.query("CREATE INDEX creators_creatorDataID ON creators(creatorDataID);");
+
+ Zotero.DB.query("DROP TABLE collectionsOld");
+ Zotero.DB.query("DROP TABLE creatorsOld");
+ Zotero.DB.query("DROP TABLE itemsOld");
+ Zotero.DB.query("DROP TABLE savedSearchesOld");
+ Zotero.DB.query("DROP TABLE tagsOld");
+
+ Zotero.DB.query("CREATE TABLE libraries (\n libraryID INTEGER PRIMARY KEY,\n libraryType TEXT NOT NULL\n);");
+ Zotero.DB.query("CREATE TABLE users (\n userID INTEGER PRIMARY KEY,\n username TEXT NOT NULL\n);");
+ Zotero.DB.query("CREATE TABLE groups (\n groupID INTEGER PRIMARY KEY,\n libraryID INT NOT NULL UNIQUE,\n name TEXT NOT NULL,\n description TEXT NOT NULL,\n editable INT NOT NULL,\n filesEditable INT NOT NULL,\n FOREIGN KEY (libraryID) REFERENCES libraries(libraryID)\n);");
+ Zotero.DB.query("CREATE TABLE groupItems (\n itemID INTEGER PRIMARY KEY,\n createdByUserID INT NOT NULL,\n lastModifiedByUserID INT NOT NULL,\n FOREIGN KEY (createdByUserID) REFERENCES users(userID),\n FOREIGN KEY (lastModifiedByUserID) REFERENCES users(userID)\n);");
+
+ Zotero.DB.query("ALTER TABLE syncDeleteLog RENAME TO syncDeleteLogOld");
+ Zotero.DB.query("DROP INDEX syncDeleteLog_timestamp");
+ Zotero.DB.query("CREATE TABLE syncDeleteLog (\n syncObjectTypeID INT NOT NULL,\n libraryID INT,\n key TEXT NOT NULL,\n timestamp INT NOT NULL,\n UNIQUE (libraryID, key),\n FOREIGN KEY (syncObjectTypeID) REFERENCES syncObjectTypes(syncObjectTypeID)\n);");
+ Zotero.DB.query("INSERT INTO syncDeleteLog SELECT syncObjectTypeID, NULL, key, timestamp FROM syncDeleteLogOld");
+ Zotero.DB.query("CREATE INDEX syncDeleteLog_timestamp ON syncDeleteLog(timestamp)");
+ Zotero.DB.query("DROP TABLE syncDeleteLogOld");
+
+ Zotero.DB.query("ALTER TABLE storageDeleteLog RENAME TO storageDeleteLogOld");
+ Zotero.DB.query("DROP INDEX storageDeleteLog_timestamp");
+ Zotero.DB.query("CREATE TABLE storageDeleteLog (\n libraryID INT,\n key TEXT NOT NULL,\n timestamp INT NOT NULL,\n PRIMARY KEY (libraryID, key)\n);");
+ Zotero.DB.query("INSERT INTO storageDeleteLog SELECT NULL, key, timestamp FROM storageDeleteLogOld");
+ Zotero.DB.query("CREATE INDEX storageDeleteLog_timestamp ON storageDeleteLog(timestamp)");
+ Zotero.DB.query("DROP TABLE storageDeleteLogOld");
+
+ Zotero.DB.query("CREATE TEMPORARY TABLE tmpUpdatedItems (itemID INTEGER PRIMARY KEY)");
+ Zotero.DB.query("INSERT INTO tmpUpdatedItems SELECT itemID FROM items NATURAL JOIN itemData WHERE fieldID=10 AND itemTypeID IN (2,9)");
+ Zotero.DB.query("UPDATE itemData SET fieldID=118 WHERE fieldID=10 AND itemID IN (SELECT itemID FROM tmpUpdatedItems)");
+ Zotero.DB.query("DROP TABLE tmpUpdatedItems");
+ }
+
+ if (i==52) {
+ Zotero.DB.query("CREATE TABLE relations (\n libraryID INT NOT NULL,\n subject TEXT NOT NULL,\n predicate TEXT NOT NULL,\n object TEXT NOT NULL,\n clientDateModified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n PRIMARY KEY (libraryID, subject, predicate, object)\n)");
+ Zotero.DB.query("CREATE INDEX relations_object ON relations(libraryID, object)")
+ }
+
+ if (i==53) {
+ Zotero.DB.query("DELETE FROM collectionItems WHERE itemID IN (SELECT itemID FROM items WHERE itemID IN (SELECT itemID FROM itemAttachments WHERE sourceItemID IS NOT NULL) OR itemID IN (SELECT itemID FROM itemNotes WHERE sourceItemID IS NOT NULL))");
+ }
}
_updateDBVersion('userdata', toVersion);
diff --git a/chrome/content/zotero/xpcom/search.js b/chrome/content/zotero/xpcom/search.js
@@ -20,20 +20,25 @@
***** END LICENSE BLOCK *****
*/
-Zotero.Search = function(searchID) {
- this._id = searchID ? searchID : null;
+Zotero.Search = function() {
+ if (arguments[0]) {
+ throw ("Zotero.Search constructor doesn't take any parameters");
+ }
+
+ this._loaded = false;
this._init();
}
Zotero.Search.prototype._init = function () {
// Public members for access by public methods -- do not access directly
+ this._id = null;
+ this._libraryID = null;
+ this._key = null;
this._name = null;
this._dateAdded = null;
this._dateModified = null;
- this._key = null;
- this._loaded = false;
this._changed = false;
this._previousData = false;
@@ -64,24 +69,25 @@ Zotero.Search.prototype.setName = function(val) {
}
-Zotero.Search.prototype.__defineGetter__('id', function () { return this._id; });
-
+Zotero.Search.prototype.__defineGetter__('objectType', function () { return 'search'; });
+Zotero.Search.prototype.__defineGetter__('id', function () { return this._get('id'); });
Zotero.Search.prototype.__defineSetter__('id', function (val) { this._set('id', val); });
-Zotero.Search.prototype.__defineSetter__('searchID', function (val) { this._set('id', val); });
+Zotero.Search.prototype.__defineGetter__('libraryID', function () { return this._get('libraryID'); });
+Zotero.Search.prototype.__defineSetter__('libraryID', function (val) { return this._set('libraryID', val); });
+Zotero.Search.prototype.__defineGetter__('key', function () { return this._get('key'); });
+Zotero.Search.prototype.__defineSetter__('key', function (val) { this._set('key', val) });
Zotero.Search.prototype.__defineGetter__('name', function () { return this._get('name'); });
Zotero.Search.prototype.__defineSetter__('name', function (val) { this._set('name', val); });
Zotero.Search.prototype.__defineGetter__('dateAdded', function () { return this._get('dateAdded'); });
Zotero.Search.prototype.__defineSetter__('dateAdded', function (val) { this._set('dateAdded', val); });
Zotero.Search.prototype.__defineGetter__('dateModified', function () { return this._get('dateModified'); });
Zotero.Search.prototype.__defineSetter__('dateModified', function (val) { this._set('dateModified', val); });
-Zotero.Search.prototype.__defineGetter__('key', function () { return this._get('key'); });
-Zotero.Search.prototype.__defineSetter__('key', function (val) { this._set('key', val); });
Zotero.Search.prototype.__defineGetter__('conditions', function (arr) { this.getSearchConditions(); });
Zotero.Search.prototype._get = function (field) {
- if (this.id && !this._loaded) {
+ if ((this._id || this._key) && !this._loaded) {
this.load();
}
return this['_' + field];
@@ -89,17 +95,27 @@ Zotero.Search.prototype._get = function (field) {
Zotero.Search.prototype._set = function (field, val) {
- if (field == 'name') {
- val = Zotero.Utilities.prototype.trim(val);
- }
-
switch (field) {
- //case 'id': // set using constructor
- case 'searchID':
- throw ("Invalid field '" + field + "' in Zotero.Search.set()");
+ case 'id':
+ case 'libraryID':
+ case 'key':
+ if (val == this['_' + field]) {
+ return;
+ }
+
+ if (this._loaded) {
+ throw ("Cannot set " + field + " after object is already loaded in Zotero.Search._set()");
+ }
+ //this._checkValue(field, val);
+ this['_' + field] = val;
+ return;
+
+ case 'name':
+ val = Zotero.Utilities.prototype.trim(val);
+ break;
}
- if (this.id) {
+ if (this.id || this.key) {
if (!this._loaded) {
this.load();
}
@@ -143,32 +159,51 @@ Zotero.Search.prototype.load = function() {
throw ('Parameter no longer allowed in Zotero.Search.load()');
}
+ var id = this._id;
+ var key = this._key;
+ var libraryID = this._libraryID;
+ var desc = id ? id : libraryID + "/" + key;
+
var sql = "SELECT S.*, "
+ "MAX(searchConditionID) AS maxID "
+ "FROM savedSearches S LEFT JOIN savedSearchConditions "
- + "USING (savedSearchID) WHERE savedSearchID=? "
- + "GROUP BY savedSearchID";
- var data = Zotero.DB.rowQuery(sql, this.id);
+ + "USING (savedSearchID) WHERE ";
+ if (id) {
+ sql += "savedSearchID=?";
+ var params = id;
+ }
+ else {
+ sql += "key=?";
+ var params = [key];
+ if (libraryID) {
+ sql += " AND libraryID=?";
+ params.push(libraryID);
+ }
+ else {
+ sql += " AND libraryID IS NULL";
+ }
+ }
+ sql += " GROUP BY savedSearchID";
+ var data = Zotero.DB.rowQuery(sql, params);
- this._init();
this._loaded = true;
if (!data) {
return;
}
- this._changed = false;
- this._previousData = false;
+ this._init();
this._id = data.savedSearchID;
+ this._libraryID = data.libraryID;
+ this._key = data.key;
this._name = data.savedSearchName;
this._dateAdded = data.dateAdded;
this._dateModified = data.dateModified;
- this._key = data.key;
this._maxSearchConditionID = data.maxID;
var sql = "SELECT * FROM savedSearchConditions "
+ "WHERE savedSearchID=? ORDER BY searchConditionID";
- var conditions = Zotero.DB.query(sql, this.id);
+ var conditions = Zotero.DB.query(sql, this._id);
for (var i in conditions) {
// Parse "condition[/mode]"
@@ -203,36 +238,14 @@ Zotero.Search.prototype.load = function() {
* For new searches, name must be set called before saving
*/
Zotero.Search.prototype.save = function(fixGaps) {
+ Zotero.Searches.editCheck(this);
+
if (!this.name) {
throw('Name not provided for saved search');
}
Zotero.DB.beginTransaction();
- // ID change
- if (this._changed.id) {
- var oldID = this._previousData.primary.id;
- var params = [this.id, oldID];
-
- Zotero.debug("Changing search id " + oldID + " to " + this.id);
-
- var row = Zotero.DB.rowQuery("SELECT * FROM savedSearches WHERE savedSearchID=?", oldID);
- // Add a new row so we can update the old rows despite FK checks
- // Use temp key due to UNIQUE constraint on key column
- Zotero.DB.query("INSERT INTO savedSearches VALUES (?, ?, ?, ?, ?)",
- [this.id, row.savedSearchName, row.dateAdded, row.dateModified, 'TEMPKEY']);
-
- Zotero.DB.query("UPDATE savedSearchConditions SET savedSearchID=? WHERE savedSearchID=?", params);
-
- Zotero.DB.query("DELETE FROM savedSearches WHERE savedSearchID=?", oldID);
- Zotero.DB.query("UPDATE savedSearches SET key=? WHERE savedSearchID=?", [row.key, this.id]);
-
- //Zotero.Searches.unload(oldID);
- Zotero.Notifier.trigger('id-change', 'search', oldID + '-' + this.id);
-
- // update caches
- }
-
var isNew = !this.id || !this.exists();
try {
@@ -243,9 +256,15 @@ Zotero.Search.prototype.save = function(fixGaps) {
var key = this.key ? this.key : this._generateKey();
var columns = [
- 'savedSearchID', 'savedSearchName', 'dateAdded', 'dateModified', 'key'
+ 'savedSearchID',
+ 'savedSearchName',
+ 'dateAdded',
+ 'dateModified',
+ 'clientDateModified',
+ 'libraryID',
+ 'key'
];
- var placeholders = ['?', '?', '?', '?', '?'];
+ var placeholders = ['?', '?', '?', '?', '?', '?', '?'];
var sqlValues = [
searchID ? { int: searchID } : null,
{ string: this.name },
@@ -254,6 +273,8 @@ Zotero.Search.prototype.save = function(fixGaps) {
// If date modified hasn't changed, use current timestamp
this._changed.dateModified ?
this.dateModified : Zotero.DB.transactionDateTime,
+ Zotero.DB.transactionDateTime,
+ this.libraryID ? this.libraryID : this.libraryID,
key
];
@@ -352,7 +373,7 @@ Zotero.Search.prototype.clone = function() {
Zotero.Search.prototype.addCondition = function(condition, operator, value, required) {
- if (this.id && !this._loaded) {
+ if ((this.id || this.key) && !this._loaded) {
this.load();
}
@@ -416,7 +437,7 @@ Zotero.Search.prototype.setScope = function (searchObj, includeChildren) {
Zotero.Search.prototype.updateCondition = function(searchConditionID, condition, operator, value, required){
- if (this.id && !this._loaded) {
+ if ((this.id || this.key) && !this._loaded) {
this.load();
}
@@ -445,7 +466,7 @@ Zotero.Search.prototype.updateCondition = function(searchConditionID, condition,
Zotero.Search.prototype.removeCondition = function(searchConditionID){
- if (this.id && !this._loaded) {
+ if ((this.id || this.key) && !this._loaded) {
this.load();
}
@@ -462,7 +483,7 @@ Zotero.Search.prototype.removeCondition = function(searchConditionID){
* for the given searchConditionID
*/
Zotero.Search.prototype.getSearchCondition = function(searchConditionID){
- if (this.id && !this._loaded) {
+ if ((this.id || this.key) && !this._loaded) {
this.load();
}
return this._conditions[searchConditionID];
@@ -474,7 +495,7 @@ Zotero.Search.prototype.getSearchCondition = function(searchConditionID){
* used in the search, indexed by searchConditionID
*/
Zotero.Search.prototype.getSearchConditions = function(){
- if (this.id && !this._loaded) {
+ if ((this.id || this.key) && !this._loaded) {
this.load();
}
var conditions = [];
@@ -495,7 +516,7 @@ Zotero.Search.prototype.getSearchConditions = function(){
Zotero.Search.prototype.hasPostSearchFilter = function() {
- if (this.id && !this._loaded) {
+ if ((this.id || this.key) && !this._loaded) {
this.load();
}
for each(var i in this._conditions){
@@ -511,7 +532,7 @@ Zotero.Search.prototype.hasPostSearchFilter = function() {
* Run the search and return an array of item ids for results
*/
Zotero.Search.prototype.search = function(asTempTable){
- if (this.id && !this._loaded) {
+ if ((this.id || this.key) && !this._loaded) {
this.load();
}
@@ -821,9 +842,10 @@ Zotero.Search.prototype.serialize = function() {
var obj = {
primary: {
id: this.id,
+ libraryID: this.libraryID,
+ key: this.key,
dateAdded: this.dateAdded,
- dateModified: this.dateModified,
- key: this.key
+ dateModified: this.dateModified
},
fields: {
name: this.name,
@@ -1095,7 +1117,8 @@ Zotero.Search.prototype._buildQuery = function(){
condSQL += "NOT ";
}
condSQL += "IN (";
- var search = new Zotero.Search(condition.value);
+ var search = new Zotero.Search();
+ search.id = condition.value;
// Check if there are any post-search filters
var hasFilter = search.hasPostSearchFilter();
@@ -1346,10 +1369,14 @@ Zotero.Search.prototype._buildQuery = function(){
case 'isNot': // excluded with NOT IN above
// Automatically cast values which might
// have been stored as integers
- if (condition.value
+ if (condition.value && typeof condition.value == 'string'
&& condition.value.match(/^[1-9]+[0-9]*$/)) {
condSQL += ' LIKE ?';
}
+ else if (condition.value === null) {
+ condSQL += ' IS NULL';
+ break;
+ }
else {
condSQL += '=?';
}
@@ -1514,7 +1541,9 @@ Zotero.Searches = new function(){
function get(id) {
var sql = "SELECT COUNT(*) FROM savedSearches WHERE savedSearchID=?";
if (Zotero.DB.valueQuery(sql, id)) {
- return new Zotero.Search(id);
+ var search = new Zotero.Search;
+ search.id = id;
+ return search;
}
return false;
}
@@ -1548,7 +1577,8 @@ Zotero.Searches = new function(){
Zotero.DB.beginTransaction();
for each(var id in ids) {
- var search = new Zotero.Search(id);
+ var search = new Zotero.Search;
+ search.id = id;
notifierData[id] = { old: search.serialize() };
var sql = "DELETE FROM savedSearchConditions WHERE savedSearchID=?";
@@ -1884,6 +1914,17 @@ Zotero.SearchConditions = new function(){
},
{
+ name: 'libraryID',
+ operators: {
+ is: true,
+ isNot: true
+ },
+ table: 'items',
+ field: 'libraryID',
+ special: true
+ },
+
+ {
name: 'annotation',
operators: {
contains: true,
diff --git a/chrome/content/zotero/xpcom/storage.js b/chrome/content/zotero/xpcom/storage.js
@@ -461,7 +461,7 @@ Zotero.Sync.Storage = new function () {
* Also marks missing files for downloading
*
* @param {Integer[]} itemIDs An optional set of item ids to check
- * @param {Object} itemModTimes Item mod times indexed by item ids
+ * @param {Object} itemModTimes Item mod times indexed by item ids
* appearing in itemIDs; if set,
* items with stored mod times
* that differ from the provided
@@ -497,8 +497,8 @@ Zotero.Sync.Storage = new function () {
do {
var chunk = itemIDs.splice(0, maxIDs);
var sql = "SELECT itemID, linkMode, path, storageModTime, syncState "
- + "FROM itemAttachments "
- + "WHERE linkMode IN (?,?) AND syncState IN (?,?)";
+ + "FROM itemAttachments JOIN items USING (itemID) "
+ + "WHERE linkMode IN (?,?) AND syncState IN (?,?) AND libraryID IS NULL";
var params = [
Zotero.Attachments.LINK_MODE_IMPORTED_FILE,
Zotero.Attachments.LINK_MODE_IMPORTED_URL,
@@ -665,7 +665,7 @@ Zotero.Sync.Storage = new function () {
*/
this.downloadFile = function (request) {
var key = request.name;
- var item = Zotero.Items.getByKey(key);
+ var item = Zotero.Items.getByLibraryAndKey(null, key);
if (!item) {
_error("Item '" + key
+ "' not found in Zotero.Sync.Storage.downloadFile()");
@@ -735,7 +735,7 @@ Zotero.Sync.Storage = new function () {
wbp.saveURI(uri, null, null, null, null, destFile);
}
catch (e) {
- request.error(e.message);
+ request.error(e);
}
});
}
@@ -918,7 +918,7 @@ Zotero.Sync.Storage = new function () {
}
var key = file.replace(/\.(zip|prop)$/, '');
- var item = Zotero.Items.getByKey(key);
+ var item = Zotero.Items.getByLibraryAndKey(null, key);
if (item) {
Zotero.debug("Skipping existing file " + file);
continue;
@@ -1006,7 +1006,8 @@ Zotero.Sync.Storage = new function () {
+ "Zotero.Sync.Storage.resetAllSyncStates()");
}
- var sql = "UPDATE itemAttachments SET syncState=?";
+ var sql = "UPDATE itemAttachments SET syncState=? WHERE itemID IN "
+ + "(SELECT itemID FROM items WHERE libraryID IS NULL)";
Zotero.DB.query(sql, [syncState]);
var sql = "DELETE FROM version WHERE schema='storage'";
@@ -1184,7 +1185,7 @@ Zotero.Sync.Storage = new function () {
*/
function _createUploadFile(request) {
var key = request.name;
- var item = Zotero.Items.getByKey(key);
+ var item = Zotero.Items.getByLibraryAndKey(null, key);
Zotero.debug("Creating zip file for item " + item.key);
try {
@@ -1222,7 +1223,7 @@ Zotero.Sync.Storage = new function () {
return true;
}
catch (e) {
- request.error(e.message);
+ request.error(e);
return false;
}
}
@@ -1273,7 +1274,7 @@ Zotero.Sync.Storage = new function () {
*/
var request = data.request;
- var item = Zotero.Items.getByKey(request.name);
+ var item = Zotero.Items.getByLibraryAndKey(null, request.name);
Zotero.Sync.Storage.getStorageModificationTime(item, function (item, mdate) {
if (!request.isRunning()) {
@@ -1378,7 +1379,7 @@ Zotero.Sync.Storage = new function () {
channel.asyncOpen(listener, null);
}
catch (e) {
- request.error(e.message);
+ request.error(e);
}
});
}
@@ -1459,7 +1460,8 @@ Zotero.Sync.Storage = new function () {
* @return {Number[]} Array of attachment itemIDs
*/
function _getFilesToDownload() {
- var sql = "SELECT itemID FROM itemAttachments WHERE syncState IN (?,?)";
+ var sql = "SELECT itemID FROM itemAttachments JOIN items USING (itemID) "
+ + "WHERE syncState IN (?,?) AND libraryID IS NULL";
return Zotero.DB.columnQuery(sql,
[
Zotero.Sync.Storage.SYNC_STATE_TO_DOWNLOAD,
@@ -1476,8 +1478,8 @@ Zotero.Sync.Storage = new function () {
* @return {Number[]} Array of attachment itemIDs
*/
function _getFilesToUpload() {
- var sql = "SELECT itemID FROM itemAttachments WHERE syncState IN (?,?) "
- + "AND linkMode IN (?,?)";
+ var sql = "SELECT itemID FROM itemAttachments JOIN items USING (itemID) "
+ + "WHERE syncState IN (?,?) AND linkMode IN (?,?) AND libraryID IS NULL";
return Zotero.DB.columnQuery(sql,
[
Zotero.Sync.Storage.SYNC_STATE_TO_UPLOAD,
@@ -2371,7 +2373,7 @@ Zotero.Sync.Storage.QueueManager = new function () {
function _reconcileConflicts() {
var objectPairs = [];
for each(var conflict in _conflicts) {
- var item = Zotero.Items.getByKey(conflict.name);
+ var item = Zotero.Items.getByLibraryAndKey(null, conflict.name);
var item1 = item.clone();
item1.setField('dateModified',
Zotero.Date.dateToSQL(new Date(conflict.localData.modTime * 1000), true));
@@ -2406,7 +2408,7 @@ Zotero.Sync.Storage.QueueManager = new function () {
// Since we're only putting cloned items into the merge window,
// we have to manually set the ids
for (var i=0; i<_conflicts.length; i++) {
- io.dataOut[i].id = Zotero.Items.getByKey(_conflicts[i].name).id;
+ io.dataOut[i].id = Zotero.Items.getByLibraryAndKey(null, _conflicts[i].name).id;
}
return io.dataOut;
@@ -2876,6 +2878,8 @@ Zotero.Sync.Storage.Request.prototype.onProgress = function (channel, progress,
Zotero.Sync.Storage.Request.prototype.error = function (msg) {
+ msg = typeof msg == 'object' ? msg.message : msg;
+
this.queue.logError(msg);
// DEBUG: ever need to stop channel?
diff --git a/chrome/content/zotero/xpcom/sync.js b/chrome/content/zotero/xpcom/sync.js
@@ -1,10 +1,4 @@
Zotero.Sync = new function() {
- this.init = init;
- this.getObjectTypeID = getObjectTypeID;
- this.getObjectTypeName = getObjectTypeName;
- this.getDeletedObjects = getDeletedObjects;
- this.purgeDeletedObjects = purgeDeletedObjects;
-
// Keep in sync with syncObjectTypes table
this.__defineGetter__('syncObjects', function () {
return {
@@ -27,8 +21,11 @@ Zotero.Sync = new function() {
tag: {
singular: 'Tag',
plural: 'Tags'
- }
-
+ },
+ relation: {
+ singular: 'Relation',
+ plural: 'Relations'
+ },
};
});
@@ -41,7 +38,7 @@ Zotero.Sync = new function() {
var _deleteLogDays = 30;
- function init() {
+ this.init = function () {
var sql = "SELECT version FROM version WHERE schema='syncdeletelog'";
if (!Zotero.DB.valueQuery(sql)) {
sql = "SELECT COUNT(*) FROM syncDeleteLog";
@@ -56,7 +53,7 @@ Zotero.Sync = new function() {
}
- function getObjectTypeID(type) {
+ this.getObjectTypeID = function (type) {
if (!_typesLoaded) {
_loadObjectTypes();
}
@@ -66,7 +63,7 @@ Zotero.Sync = new function() {
}
- function getObjectTypeName(typeID, plural) {
+ this.getObjectTypeName = function (typeID, plural) {
if (!_typesLoaded) {
_loadObjectTypes();
}
@@ -79,9 +76,9 @@ Zotero.Sync = new function() {
/**
* @param {Date} olderThanDate Retrieve objects last updated before this date
* @param {Date} newerThanDate Retrieve objects last updated after this date
- * @return {Object} { items: [123, 234, ...], creators: [321, 432, ...], ... }
+ * @param {Zotero.Sync.Server.ObjectKeySet}
*/
- this.getObjectsByDate = function (olderThanDate, newerThanDate) {
+ this.getObjectsByDate = function (olderThanDate, newerThanDate, objectKeySet) {
var funcName = "Zotero.Sync.getObjectsByDate()";
if (olderThanDate && olderThanDate.constructor.name != 'Date') {
throw ("olderThanDate must be a Date or FALSE in " + funcName)
@@ -90,61 +87,43 @@ Zotero.Sync = new function() {
throw ("newerThanDate must be a Date or FALSE in " + funcName)
}
- // If dates overlap, retrieve all objects
- if (!olderThanDate && !newerThanDate) {
- var all = true;
- }
- else if (olderThanDate && newerThanDate && olderThanDate > newerThanDate) {
+ // If either not set (first sync) or dates overlap, retrieve all objects
+ if ((!olderThanDate || !newerThanDate) ||
+ (olderThanDate && newerThanDate && olderThanDate > newerThanDate)) {
olderThanDate = null;
newerThanDate = null;
var all = true;
}
- var updatedIDs = {};
- for each(var syncObject in this.syncObjects) {
- var Types = syncObject.plural; // 'Items'
- var types = syncObject.plural.toLowerCase(); // 'items'
+ for (var type in this.syncObjects) {
+ var Types = this.syncObjects[type].plural; // 'Items'
+ var types = Types.toLowerCase(); // 'items'
Zotero.debug("Getting updated local " + types);
if (olderThanDate) {
var earlierIDs = Zotero[Types].getOlder(olderThanDate);
if (earlierIDs) {
- updatedIDs[types] = earlierIDs;
+ objectKeySet.addIDs(type, earlierIDs);
}
}
if (newerThanDate || all) {
var laterIDs = Zotero[Types].getNewer(newerThanDate);
if (laterIDs) {
- if (updatedIDs[types]) {
- updatedIDs[types].concat(laterIDs);
- }
- else {
- updatedIDs[types] = laterIDs;
- }
+ objectKeySet.addIDs(type, laterIDs);
}
}
-
- if (!updatedIDs[types]) {
- updatedIDs[types] = [];
- }
}
- return updatedIDs;
}
/**
- * @param object lastSyncDate JS Date object
- * @return mixed
- * {
- * items: [ 'ABCD1234', 'BCDE2345', ... ]
- * creators: [ 'ABCD1234', 'BCDE2345', ... ],
- * ...
- * }
- * or FALSE if none or -1 if last sync time is before start of log
+ * @param {Date} lastSyncDate JS Date object
+ * @param {Zotero.Sync.Server.ObjectKeySet}
+ * @return TRUE if found, FALSE if none, or -1 if last sync time is before start of log
*/
- function getDeletedObjects(lastSyncDate) {
+ this.getDeletedObjects = function (lastSyncDate, objectKeySet) {
if (lastSyncDate && lastSyncDate.constructor.name != 'Date') {
throw ('lastSyncDate must be a Date or FALSE in '
+ 'Zotero.Sync.getDeletedObjects()')
@@ -162,7 +141,7 @@ Zotero.Sync = new function() {
}
var param = false;
- var sql = "SELECT syncObjectTypeID, key FROM syncDeleteLog";
+ var sql = "SELECT syncObjectTypeID, libraryID, key FROM syncDeleteLog";
if (lastSyncDate) {
param = Zotero.Date.toUnixTimestamp(lastSyncDate);
sql += " WHERE timestamp>?";
@@ -174,24 +153,30 @@ Zotero.Sync = new function() {
return false;
}
- var deletedKeys = {};
- for each(var syncObject in this.syncObjects) {
- deletedKeys[syncObject.plural.toLowerCase()] = [];
+ var keys = {};
+ for (var type in this.syncObjects) {
+ keys[type] = [];
}
+ var type;
for each(var row in rows) {
- var type = this.getObjectTypeName(row.syncObjectTypeID);
- type = this.syncObjects[type].plural.toLowerCase()
- deletedKeys[type].push(row.key);
+ type = this.getObjectTypeName(row.syncObjectTypeID);
+ keys[type].push({
+ libraryID: row.libraryID,
+ key: row.key
+ });
+ }
+
+ for (var type in keys) {
+ objectKeySet.addLibraryKeyPairs(type, keys[type]);
}
- return deletedKeys;
}
/**
* @param int deleteOlderThan Unix timestamp
*/
- function purgeDeletedObjects(deleteOlderThan) {
+ this.purgeDeletedObjects = function (deleteOlderThan) {
if (isNaN(parseInt(deleteOlderThan))) {
throw ("Invalid timestamp '" + deleteOlderThan
+ "' in Zotero.Sync.purgeDeletedObjects");
@@ -214,6 +199,110 @@ Zotero.Sync = new function() {
+Zotero.Sync.ObjectKeySet = function () {
+ // Set up key holders for different types
+ var syncTypes = Zotero.Sync.syncObjects;
+ for each(var type in syncTypes) {
+ this[type.plural.toLowerCase()] = {};
+ }
+}
+
+
+Zotero.Sync.ObjectKeySet.prototype.addIDs = function (type, ids) {
+ var Types = Zotero.Sync.syncObjects[type].plural;
+ var types = Types.toLowerCase();
+
+ var obj, libraryID, key;
+ for each(var id in ids) {
+ obj = Zotero[Types].get(id);
+ libraryID = obj.libraryID;
+ if (!libraryID) {
+ libraryID = 0; // current user
+ }
+ key = obj.key;
+ if (!this[types][libraryID]) {
+ this[types][libraryID] = {};
+ }
+ this[types][libraryID][key] = true;
+ }
+}
+
+
+/**
+ * @param {String} type Sync object type (e.g., 'item', 'collection')
+ * @param {Object[]} keyPairs Array of objects with 'libraryID' and 'key'
+ */
+Zotero.Sync.ObjectKeySet.prototype.addLibraryKeys = function (type, libraryID, keys) {
+ var Types = Zotero.Sync.syncObjects[type].plural;
+ var types = Types.toLowerCase();
+
+ var key;
+ for each(var key in keys) {
+ if (!libraryID) {
+ libraryID = 0; // current user
+ }
+ if (!this[types][libraryID]) {
+ this[types][libraryID] = {};
+ }
+ this[types][libraryID][key] = true;
+ }
+}
+
+
+/**
+ * @param {String} type Sync object type (e.g., 'item', 'collection')
+ * @param {Object[]} keyPairs Array of objects with 'libraryID' and 'key'
+ */
+Zotero.Sync.ObjectKeySet.prototype.addLibraryKeyPairs = function (type, keyPairs) {
+ var Types = Zotero.Sync.syncObjects[type].plural;
+ var types = Types.toLowerCase();
+
+ var libraryID, key;
+ for each(var pair in keyPairs) {
+ libraryID = pair.libraryID;
+ if (!libraryID) {
+ libraryID = 0; // current user
+ }
+ key = pair.key;
+ if (!this[types][libraryID]) {
+ this[types][libraryID] = {};
+ }
+ this[types][libraryID][key] = true;
+ }
+}
+
+
+Zotero.Sync.ObjectKeySet.prototype.hasLibraryKey = function (type, libraryID, key) {
+ var Types = Zotero.Sync.syncObjects[type].plural;
+ var types = Types.toLowerCase();
+
+ if (!libraryID) {
+ libraryID = 0;
+ }
+
+ return this[types] && this[types][libraryID] && this[types][libraryID][key];
+}
+
+
+Zotero.Sync.ObjectKeySet.prototype.removeLibraryKeyPairs = function (type, keyPairs) {
+ var Types = Zotero.Sync.syncObjects[type].plural;
+ var types = Types.toLowerCase();
+
+ var libraryID, key;
+ for each(var pair in keyPairs) {
+ libraryID = pair.libraryID;
+ if (!libraryID) {
+ libraryID = 0; // current user
+ }
+ key = pair.key;
+ if (this[types][libraryID]) {
+ delete this[types][libraryID][key];
+ }
+ }
+}
+
+
+
/**
* Notifier observer to add deleted objects to syncDeleteLog/storageDeleteLog
* plus related methods
@@ -286,12 +375,12 @@ Zotero.Sync.EventListener = new function () {
Zotero.DB.beginTransaction();
if (event == 'delete') {
- var sql = "INSERT INTO syncDeleteLog VALUES (?, ?, ?)";
+ var sql = "INSERT INTO syncDeleteLog VALUES (?, ?, ?, ?)";
var syncStatement = Zotero.DB.getStatement(sql);
if (isItem && Zotero.Sync.Storage.active) {
var storageEnabled = true;
- var sql = "INSERT INTO storageDeleteLog VALUES (?, ?)";
+ var sql = "INSERT INTO storageDeleteLog VALUES (?, ?, ?)";
var storageStatement = Zotero.DB.getStatement(sql);
}
var storageBound = false;
@@ -307,6 +396,7 @@ Zotero.Sync.EventListener = new function () {
}
var oldItem = extraData[ids[i]].old;
+ var libraryID = oldItem.primary.libraryID;
var key = oldItem.primary.key;
if (!key) {
@@ -315,8 +405,9 @@ Zotero.Sync.EventListener = new function () {
}
syncStatement.bindInt32Parameter(0, objectTypeID);
- syncStatement.bindStringParameter(1, key);
- syncStatement.bindInt32Parameter(2, ts);
+ syncStatement.bindInt32Parameter(1, libraryID);
+ syncStatement.bindStringParameter(2, key);
+ syncStatement.bindInt32Parameter(3, ts);
if (storageEnabled &&
oldItem.primary.itemType == 'attachment' &&
@@ -324,8 +415,9 @@ Zotero.Sync.EventListener = new function () {
Zotero.Attachments.LINK_MODE_IMPORTED_FILE,
Zotero.Attachments.LINK_MODE_IMPORTED_URL
].indexOf(oldItem.attachment.linkMode) != -1) {
- storageStatement.bindStringParameter(0, key);
- storageStatement.bindInt32Parameter(1, ts);
+ storageStatement.bindInt32Parameter(0, libraryID);
+ storageStatement.bindStringParameter(1, key);
+ storageStatement.bindInt32Parameter(2, ts);
storageBound = true;
}
@@ -730,7 +822,7 @@ Zotero.Sync.Server = new function () {
});
this.nextLocalSyncDate = false;
- this.apiVersion = 4;
+ this.apiVersion = 5;
default xml namespace = '';
@@ -809,10 +901,12 @@ Zotero.Sync.Server = new function () {
return;
}
+ /*
if (!_sessionLock) {
Zotero.Sync.Server.lock(Zotero.Sync.Server.sync, callback);
return;
}
+ */
if (_syncInProgress) {
_error("Sync operation already in progress");
@@ -830,7 +924,8 @@ Zotero.Sync.Server = new function () {
}
var body = _apiVersionComponent
+ '&' + Zotero.Sync.Server.sessionIDComponent
- + '&lastsync=' + lastsync;
+ + '&lastsync=' + lastsync
+ + '&lock=1';
Zotero.Utilities.HTTP.doPost(url, body, function (xmlhttp) {
Zotero.debug(xmlhttp.responseText);
@@ -847,7 +942,12 @@ Zotero.Sync.Server = new function () {
var response = xmlhttp.responseXML.childNodes[0];
if (response.firstChild.tagName == 'error') {
- // handle error
+ var resetCallback = function () {
+ Zotero.Sync.Server.sync(callback);
+ };
+ if (_checkServerSessionLock(response.firstChild, resetCallback)) {
+ return;
+ }
_error(response.firstChild.firstChild.nodeValue);
}
@@ -857,6 +957,25 @@ Zotero.Sync.Server = new function () {
Zotero.DB.beginTransaction();
try {
+ var userID = parseInt(xml.@userID);
+ var libraryID = parseInt(xml.@defaultLibraryID);
+ if (!_checkSyncUser(userID, libraryID)) {
+ Zotero.debug("Sync cancelled");
+ Zotero.DB.rollbackTransaction();
+ Zotero.Sync.Server.unlock(function () {
+ if (callback) {
+ Zotero.Sync.Runner.setSyncIcon();
+ callback();
+ }
+ else {
+ Zotero.Sync.Runner.reset();
+ Zotero.Sync.Runner.next();
+ }
+ });
+ _syncInProgress = false;
+ return;
+ }
+
Zotero.UnresponsiveScriptIndicator.disable();
var earliestRemoteDate = parseInt(xml.@earliest) ?
@@ -867,25 +986,17 @@ Zotero.Sync.Server = new function () {
new Date(lastLocalSyncTime * 1000) : false;
var syncSession = new Zotero.Sync.Server.Session;
+
// Fetch old objects not on server (due to a clear) and new
- // objects added since last sync
- if (earliestRemoteDate && lastLocalSyncDate) {
- syncSession.uploadIDs.updated = Zotero.Sync.getObjectsByDate(
- earliestRemoteDate, lastLocalSyncDate
- );
- }
- // Fetch all local objects
- else {
- syncSession.uploadIDs.updated = Zotero.Sync.getObjectsByDate();
- }
+ // objects added since last sync, or all local objects if neither is set
+ Zotero.Sync.getObjectsByDate(
+ earliestRemoteDate, lastLocalSyncDate, syncSession.uploadKeys.updated
+ );
- var deleted = Zotero.Sync.getDeletedObjects(lastLocalSyncDate);
+ var deleted = Zotero.Sync.getDeletedObjects(lastLocalSyncDate, syncSession.uploadKeys.deleted);
if (deleted == -1) {
_error('Sync delete log starts after last sync date in Zotero.Sync.Server.sync()');
}
- if (deleted) {
- syncSession.uploadIDs.deleted = deleted;
- }
var nextLocalSyncDate = Zotero.DB.transactionDate;
var nextLocalSyncTime = Zotero.Date.toUnixTimestamp(nextLocalSyncDate);
@@ -1255,6 +1366,9 @@ Zotero.Sync.Server = new function () {
Zotero.DB.query("DELETE FROM syncDeleteLog");
Zotero.DB.query("DELETE FROM storageDeleteLog");
+ sql = "DELETE FROM settings WHERE setting='account' AND "
+ + "key IN ('userID', 'username')";
+ Zotero.DB.query(sql);
sql = "INSERT INTO version VALUES ('syncdeletelog', ?)";
Zotero.DB.query(sql, Zotero.Date.getUnixTimestamp());
@@ -1293,6 +1407,31 @@ Zotero.Sync.Server = new function () {
function _checkResponse(xmlhttp) {
if (!xmlhttp.responseText) {
+ // Check SSL cert
+ var channel = xmlhttp.channel;
+ if (!channel instanceof Ci.nsIChannel) {
+ _error('No HTTPS channel available');
+ }
+ var secInfo = channel.securityInfo;
+ if (secInfo instanceof Ci.nsITransportSecurityInfo) {
+ secInfo.QueryInterface(Ci.nsITransportSecurityInfo);
+ if ((secInfo.securityState & Ci.nsIWebProgressListener.STATE_IS_INSECURE) == Ci.nsIWebProgressListener.STATE_IS_INSECURE) {
+ var url = channel.name;
+ var ios = Components.classes["@mozilla.org/network/io-service;1"].
+ getService(Components.interfaces.nsIIOService);
+ try {
+ var uri = ios.newURI(url, null, null);
+ var host = uri.host;
+ }
+ catch (e) {
+ Zotero.debug(e);
+ }
+ _error("SSL certificate error connecting to " + host);
+ }
+ else if ((secInfo.securityState & Ci.nsIWebProgressListener.STATE_IS_BROKEN) == Ci.nsIWebProgressListener.STATE_IS_BROKEN) {
+ _error("SSL connection error");
+ }
+ }
_error('Empty response from server');
}
@@ -1329,6 +1468,7 @@ Zotero.Sync.Server = new function () {
return false;
}
var lastAccessTime = errorNode.getAttribute('lastAccessTime');
+ var relativeDateStr = Zotero.Date.toRelativeDate(lastAccessTime * 1000);
var ipAddress = errorNode.getAttribute('ipAddress');
var pr = Components.classes["@mozilla.org/network/default-prompt;1"]
@@ -1339,7 +1479,7 @@ Zotero.Sync.Server = new function () {
var index = pr.confirmEx(
Zotero.getString('general.warning'),
// TODO: localize
- "Another sync operation, started at " + lastAccessTime + " from "
+ "Another sync operation, started " + relativeDateStr + " from "
+ (ipAddress
? "another IP address (" + ipAddress + ")"
: "the current IP address")
@@ -1366,6 +1506,75 @@ Zotero.Sync.Server = new function () {
}
+ /**
+ * Make sure we're syncing with the same account we used last time
+ *
+ * @return TRUE if sync should continue, FALSE if cancelled
+ */
+ function _checkSyncUser(userID, libraryID) {
+ var sql = "SELECT key, value FROM settings WHERE "
+ + "setting='account' AND key='username'";
+ var lastUsername = Zotero.DB.valueQuery(sql);
+ var username = Zotero.Sync.Server.username;
+ var lastUserID = Zotero.userID;
+ var lastLibraryID = Zotero.libraryID;
+
+ if (lastUserID && lastUserID != userID) {
+ var pr = Components.classes["@mozilla.org/network/default-prompt;1"]
+ .createInstance(Components.interfaces.nsIPrompt);
+ var buttonFlags = (pr.BUTTON_POS_0) * (pr.BUTTON_TITLE_IS_STRING)
+ + (pr.BUTTON_POS_1) * (pr.BUTTON_TITLE_CANCEL)
+ + (pr.BUTTON_POS_2) * (pr.BUTTON_TITLE_IS_STRING)
+ + pr.BUTTON_POS_1_DEFAULT
+ + pr.BUTTON_DELAY_ENABLE;
+ var index = pr.confirmEx(
+ Zotero.getString('general.warning'),
+ // TODO: localize
+ "This Zotero database was last synced with a different "
+ + "zotero.org account ('" + lastUsername + "') from the "
+ + "current one ('" + username + "'). "
+ + "If you continue, local Zotero data will be "
+ + "combined with data from the '" + username + "' account "
+ + "stored on the server.\n\n"
+ + "To avoid combining data, revert to the '"
+ + lastUsername + "' account or use the Reset options "
+ + "in the Sync pane of the Zotero preferences.",
+ buttonFlags,
+ "Sync",
+ null,
+ "Open Sync Preferences",
+ null, {}
+ );
+
+ if (index > 0) {
+ if (index == 1) {
+ // Cancel
+ }
+ else if (index == 2) {
+ var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
+ .getService(Components.interfaces.nsIWindowMediator);
+ var lastWin = wm.getMostRecentWindow("navigator:browser");
+ lastWin.ZoteroPane.openPreferences('zotero-prefpane-sync');
+ }
+
+ return false;
+ }
+ }
+
+ if (lastUserID != userID || lastLibraryID != libraryID) {
+ Zotero.userID = userID;
+ Zotero.libraryID = libraryID;
+ }
+
+ if (lastUsername != username) {
+ var sql = "REPLACE INTO settings VALUES ('account', 'username', ?)";
+ Zotero.DB.query(sql, username);
+ }
+
+ return true;
+ }
+
+
function _invalidSession(xmlhttp) {
if (xmlhttp.responseXML.childNodes[0].firstChild.tagName != 'error') {
return false;
@@ -1443,97 +1652,83 @@ Zotero.BufferedInputListener.prototype = {
* Stores information about a sync session
*
* @class
- * @property {Object} uploadIDs IDs to be uploaded to server
- *
- * {
- * updated: {
- * items: [123, 234, 345, 456],
- * creators: [321, 432, 543, 654]
- * },
- * changed: {
- * items: {
- * 1234: { oldID: 1234, newID: 5678 }, ...
- * },
- * creators: {
- * 1234: { oldID: 1234, newID: 5678 }, ...
- * }
- * },
- * deleted: {
- * items: [
- * 'ABCD1234', 'BCDE2345', ...
- * ],
- * creators: [
- * 'ABCD1234', 'BCDE2345', ...
- * ]
- * }
- * };
+ * @property {Object} uploadKeys Keys to upload to server
+ * @property {Zotero.Sync.ObjectKeySet} uploadKeys.updated
+ * @property {Zotero.Sync.ObjectKeySet} uploadKeys.deleted
*/
Zotero.Sync.Server.Session = function () {
- this.uploadIDs = {};
- this.uploadIDs.updated = {};
- this.uploadIDs.changed = {};
- this.uploadIDs.deleted = {};
-
- for each(var syncObject in Zotero.Sync.syncObjects) {
- var types = syncObject.plural.toLowerCase(); // 'items'
-
- this.uploadIDs.updated[types] = [];
- this.uploadIDs.changed[types] = {};
- this.uploadIDs.deleted[types] = [];
- }
+ this.uploadKeys = {};
+ this.uploadKeys.updated = new Zotero.Sync.ObjectKeySet;
+ this.uploadKeys.deleted = new Zotero.Sync.ObjectKeySet;
}
-Zotero.Sync.Server.Session.prototype.addToUpdated = function (syncObjectTypeName, ids) {
- var pluralType = Zotero.Sync.syncObjects[syncObjectTypeName].plural.toLowerCase();
- var updated = this.uploadIDs.updated[pluralType];
-
- ids = Zotero.flattenArguments(ids);
- for each(var id in ids) {
- if (updated.indexOf(id) == -1) {
- updated.push(id);
- }
- }
+Zotero.Sync.Server.Session.prototype.addToUpdated = function (objs) {
+ this._addToKeySet('updated', objs);
+}
+
+Zotero.Sync.Server.Session.prototype.addToDeleted = function (objs) {
+ this._addToKeySet('deleted', objs);
}
-Zotero.Sync.Server.Session.prototype.removeFromUpdated = function (syncObjectTypeName, ids) {
- var pluralType = Zotero.Sync.syncObjects[syncObjectTypeName].plural.toLowerCase();
- var updated = this.uploadIDs.updated[pluralType];
-
- ids = Zotero.flattenArguments(ids);
- var index;
- for each(var id in ids) {
- index = updated.indexOf(id);
- if (index != -1) {
- updated.splice(index, 1);
- }
- }
+Zotero.Sync.Server.Session.prototype.objectInUpdated = function (obj) {
+ var type = obj.objectType;
+ return this.uploadKeys.updated.hasLibraryKey(type, obj.libraryID, obj.key);
}
-Zotero.Sync.Server.Session.prototype.addToDeleted = function (syncObjectTypeName, key) {
- var pluralType = Zotero.Sync.syncObjects[syncObjectTypeName].plural.toLowerCase();
- var deleted = this.uploadIDs.deleted[pluralType];
- if (deleted.indexOf(key) != -1) {
- return;
- }
- deleted.push(key);
+Zotero.Sync.Server.Session.prototype.objectInDeleted = function (obj) {
+ var type = obj.objectType;
+ return this.uploadKeys.deleted.hasLibraryKey(type, obj.libraryID, obj.key);
}
-Zotero.Sync.Server.Session.prototype.removeFromDeleted = function (syncObjectTypeName, key) {
- var pluralType = Zotero.Sync.syncObjects[syncObjectTypeName].plural.toLowerCase();
- var deleted = this.uploadIDs.deleted[pluralType];
- for (var i=0; i<deleted.length; i++) {
- if (deleted[i] == key) {
- deleted.splice(i, 1);
- i--;
- }
+Zotero.Sync.Server.Session.prototype.removeFromUpdated = function (objs) {
+ this._removeFromKeySet('updated', objs);
+}
+
+
+Zotero.Sync.Server.Session.prototype.removeFromDeleted = function (objs) {
+ this._removeFromKeySet('deleted', objs);
+}
+
+
+Zotero.Sync.Server.Session.prototype._addToKeySet = function (keySet, objs) {
+ objs = Zotero.flattenArguments(objs);
+
+ var type = objs[0].objectType;
+
+ var keyPairs = [];
+ for each(var obj in objs) {
+ keyPairs.push({
+ libraryID: obj.libraryID,
+ key: obj.key
+ });
}
+ Zotero.debug('a');
+ this.uploadKeys[keySet].addLibraryKeyPairs(type, keyPairs)
}
+Zotero.Sync.Server.Session.prototype._removeFromKeySet = function (keySet, objs) {
+ if (!objs) {
+ throw ("No objects provided in Zotero.Sync.Server.Session._removeFromKeySet()");
+ }
+ objs = Zotero.flattenArguments(objs);
+
+ var type = objs[0].objectType;
+
+ var keyPairs = [];
+ for each(var obj in objs) {
+ keyPairs.push({
+ libraryID: obj.libraryID,
+ key: obj.key
+ });
+ }
+ this.uploadKeys[keySet].removeLibraryKeyPairs(type, keyPairs)
+}
+
Zotero.Sync.Server.Data = new function() {
this.processUpdatedXML = processUpdatedXML;
@@ -1555,73 +1750,6 @@ Zotero.Sync.Server.Data = new function() {
/**
- * Reorder XML nodes for parent/child relationships, etc.
- *
- * @param {E4X} xml
- */
- function _preprocessUpdatedXML(xml) {
- if (xml.collections.length()) {
- var collections = xml.collections.children();
- var orderedCollections = <collections/>;
- var collectionIDHash = {};
-
- for (var i=0; i<collections.length(); i++) {
- // Build a hash of all collection ids
- collectionIDHash[collections[i].@id.toString()] = true;
-
- // Pull out top-level collections
- if (!collections[i].@parent.toString()) {
- orderedCollections.collection += collections[i];
- delete collections[i];
- i--;
- }
- }
-
- // Pull out all collections pointing to parents that
- // aren't present, which we assume already exist
- for (var i=0; i<collections.length(); i++) {
- if (!collectionIDHash[collections[i].@parent]) {
- orderedCollections.collection += collections[i]
- delete collections[i];
- i--;
- }
- }
-
- // Insert children directly under parents
- for (var i=0; i<orderedCollections.children().length(); i++) {
- for (var j=0; j<collections.length(); j++) {
- if (collections[j].@parent.toString() ==
- orderedCollections.children()[i].@id.toString()) {
- // Make a clone of object, since otherwise
- // delete below erases inserted item as well
- // (which only seems to happen with
- // insertChildBefore(), not += above)
- var newChild = new XML(collections[j].toXMLString())
-
- // If last top-level, just append
- if (i == orderedCollections.children().length() - 1) {
- orderedCollections.appendChild(newChild);
- }
- else {
- orderedCollections.insertChildBefore(
- orderedCollections.children()[i+1],
- newChild
- );
- }
- delete collections[j];
- j--;
- }
- }
- }
-
- xml.collections = orderedCollections;
- }
-
- return xml;
- }
-
-
- /**
* Pull out collections from delete queue in XML
*
* @param {XML} xml
@@ -1631,7 +1759,12 @@ Zotero.Sync.Server.Data = new function() {
var keys = [];
if (xml.deleted && xml.deleted.collections) {
for each(var xmlNode in xml.deleted.collections.collection) {
- keys.push(xmlNode.@key.toString());
+ var libraryID = xmlNode.@libraryID.toString();
+ libraryID = libraryID ? parseInt(libraryID) : null;
+ keys.push({
+ libraryID: libraryID,
+ key: xmlNode.@key.toString()
+ });
}
}
return keys;
@@ -1646,23 +1779,51 @@ Zotero.Sync.Server.Data = new function() {
Zotero.DB.beginTransaction();
- xml = _preprocessUpdatedXML(xml);
var deletedCollectionKeys = _getDeletedCollectionKeys(xml);
var remoteCreatorStore = {};
var relatedItemsStore = {};
var itemStorageModTimes = {};
+ if (xml.groups.length()) {
+ Zotero.debug("Processing remotely changed groups");
+ for each(var xmlNode in xml.groups.group) {
+ var group = Zotero.Sync.Server.Data.xmlToGroup(xmlNode);
+ group.save();
+ }
+ }
+
+ if (xml.deleted.groups.toString()) {
+ Zotero.debug("Processing remotely deleted groups");
+ var groupIDs = xml.deleted.groups.toString().split(' ');
+ Zotero.debug(groupIDs);
+
+ for each(var groupID in groupIDs) {
+ var group = Zotero.Groups.get(groupID);
+ if (!group) {
+ continue;
+ }
+ // TODO: prompt to save
+
+ Zotero.Notifier.disable();
+
+ // TODO: figure out a better way to do this
+ var notifierData = {};
+ notifierData[groupID] = group.serialize();
+ group.erase();
+
+ Zotero.Notifier.enable();
+
+ Zotero.Notifier.trigger('delete', 'group', groupID, notifierData);
+ }
+ }
+
for each(var syncObject in Zotero.Sync.syncObjects) {
var Type = syncObject.singular; // 'Item'
var Types = syncObject.plural; // 'Items'
var type = Type.toLowerCase(); // 'item'
var types = Types.toLowerCase(); // 'items'
- if (!xml[types]) {
- continue;
- }
-
var toSave = [];
var toDelete = [];
var toReconcile = [];
@@ -1674,207 +1835,176 @@ Zotero.Sync.Server.Data = new function() {
typeloop:
for each(var xmlNode in xml[types][type]) {
- Zotero.debug("Processing remote " + type + " " + xmlNode.@id, 4);
+ var libraryID = xmlNode.@libraryID.toString();
+ var key = xmlNode.@key.toString();
+ var objLibraryKeyHash = Zotero[Types].makeLibraryKeyHash(libraryID, key);
+
+ Zotero.debug("Processing remote " + type + " " + libraryID + "/" + key, 4);
var isNewObject;
var localDelete = false;
- // Get local object with same id
- var obj = Zotero[Types].get(parseInt(xmlNode.@id));
+ // Get local object with same library and key
+ var obj = Zotero[Types].getByLibraryAndKey(libraryID, key);
if (obj) {
- // Key match -- same item
- if (obj.key == xmlNode.@key.toString()) {
- Zotero.debug("Matching local " + type + " exists", 4);
- isNewObject = false;
+ Zotero.debug("Matching local " + type + " exists", 4);
+ isNewObject = false;
+
+ var objDate = Zotero.Date.sqlToDate(obj.dateModified, true);
+
+ // Local object has been modified since last sync
+ if ((objDate > lastLocalSyncDate &&
+ objDate < Zotero.Sync.Server.nextLocalSyncDate)
+ // Check for object in updated array, since it might
+ // have been modified during sync process, making its
+ // date equal to Zotero.Sync.Server.nextLocalSyncDate
+ // and therefore excluded above
+ || syncSession.objectInUpdated(obj)) {
- var objDate = Zotero.Date.sqlToDate(obj.dateModified, true);
+ Zotero.debug("Local " + type + " " + obj.id
+ + " has been modified since last sync", 4);
- // Local object has been modified since last sync
- if ((objDate > lastLocalSyncDate &&
- objDate < Zotero.Sync.Server.nextLocalSyncDate)
- // Check for object in updated array, since it might
- // have been modified during sync process, making its
- // date equal to Zotero.Sync.Server.nextLocalSyncDate
- // and therefore excluded above (example: an item
- // linked to a creator whose id changed)
- || syncSession.uploadIDs.updated[types].indexOf(obj.id) != -1) {
-
- Zotero.debug("Local " + type + " " + obj.id
- + " has been modified since last sync", 4);
-
- // Merge and store related items, since CR doesn't
- // affect related items
- if (type == 'item') {
- // Remote
- var related = xmlNode.related.toString();
- related = related ? related.split(' ') : [];
- // Local
- for each(var relID in obj.relatedItems) {
- if (related.indexOf(relID) == -1) {
- related.push(relID);
- }
+ // Merge and store related items, since CR doesn't
+ // affect related items
+ if (type == 'item') {
+ // Remote
+ var relKeys = xmlNode.related.toString();
+ relKeys = relKeys ? relKeys.split(' ') : [];
+ // Local
+ for each(var relID in obj.relatedItems) {
+ var relKey = Zotero.Items.get(relID).key;
+ if (relKeys.indexOf(relKey) == -1) {
+ relKeys.push(relKey);
}
- if (related.length) {
- relatedItemsStore[obj.id] = related;
- }
- Zotero.Sync.Server.Data.removeMissingRelatedItems(xmlNode);
+ }
+ if (relKeys.length) {
+ relatedItemsStore[objLibraryKeyHash] = relKeys;
+ }
+ Zotero.Sync.Server.Data.removeMissingRelatedItems(xmlNode);
+ }
+
+ var remoteObj = Zotero.Sync.Server.Data['xmlTo' + Type](xmlNode);
+
+ // Some types we don't bother to reconcile
+ if (_noMergeTypes.indexOf(type) != -1) {
+ // If local is newer, send to server
+ if (obj.dateModified > remoteObj.dateModified) {
+ Zotero.debug('c');
+ syncSession.addToUpdated(obj);
+ continue;
}
- var remoteObj = Zotero.Sync.Server.Data['xmlTo' + Type](xmlNode);
+ // Overwrite local below
+ }
+ // Mark other types for conflict resolution
+ else {
+ var reconcile = false;
- // Some types we don't bother to reconcile
- if (_noMergeTypes.indexOf(type) != -1) {
- // If local is newer, send to server
- if (obj.dateModified > remoteObj.dateModified) {
- syncSession.addToUpdated(type, obj.id);
+ // Skip item if dateModified is the only modified
+ // field (and no linked creators changed)
+ switch (type) {
+ // Will be handled by item CR for now
+ case 'creator':
+ remoteCreatorStore[Zotero.Creators.getLibraryKeyHash(remoteObj)] = remoteObj;
+ syncSession.removeFromUpdated(obj);
continue;
- }
-
- // Overwrite local below
- }
- // Mark other types for conflict resolution
- else {
- var reconcile = false;
-
- // Skip item if dateModified is the only modified
- // field (and no linked creators changed)
- switch (type) {
- // Will be handled by item CR for now
- case 'creator':
- remoteCreatorStore[remoteObj.id] = remoteObj;
- syncSession.removeFromUpdated(type, obj.id);
- continue;
+
+ case 'item':
+ var diff = obj.diff(remoteObj, false, ["dateModified"]);
+ Zotero.debug('diff');
+ Zotero.debug(diff);
+ if (!diff) {
+ // Check if creators changed
+ var creatorsChanged = false;
- case 'item':
- var diff = obj.diff(remoteObj, false, true);
- if (!diff) {
- // Check if creators changed
- var creatorsChanged = false;
-
- var creators = obj.getCreators();
- var remoteCreators = remoteObj.getCreators();
-
- if (creators.length != remoteCreators.length) {
- creatorsChanged = true;
- }
- else {
- creators = creators.concat(remoteCreators);
- for each(var creator in creators) {
- var r = remoteCreatorStore[creator.ref.id];
- // Doesn't include dateModified
- if (r && !r.equals(creator.ref)) {
- creatorsChanged = true;
- break;
- }
+ var creators = obj.getCreators();
+ var remoteCreators = remoteObj.getCreators();
+
+ if (creators.length != remoteCreators.length) {
+ Zotero.debug('Creators have changed');
+ creatorsChanged = true;
+ }
+ else {
+ creators = creators.concat(remoteCreators);
+ for each(var creator in creators) {
+ var r = remoteCreatorStore[Zotero.Creators.getLibraryKeyHash(creator.ref)];
+ // Doesn't include dateModified
+ if (r && !r.equals(creator.ref)) {
+ Zotero.debug('=');
+ creatorsChanged = true;
+ break;
}
- }
- if (!creatorsChanged) {
- syncSession.removeFromUpdated(type, obj.id);
- continue;
+ Zotero.debug('-');
}
}
-
- reconcile = true;
- break;
-
- case 'collection':
- var changed = _mergeCollection(obj, remoteObj);
- if (!changed) {
- syncSession.removeFromUpdated(type, obj.id);
+ if (!creatorsChanged) {
+ syncSession.removeFromUpdated(obj);
+ continue;
}
- continue;
+ }
- case 'tag':
- var changed = _mergeTag(obj, remoteObj);
- if (!changed) {
- syncSession.removeFromUpdated(type, obj.id);
- }
- continue;
- }
-
- if (!reconcile) {
- Zotero.debug(obj);
- Zotero.debug(remoteObj);
- var msg = "Reconciliation unimplemented for " + types;
- alert(msg);
- throw(msg);
- }
-
- // TODO: order reconcile by parent/child?
+ /*
+ if (obj.deleted && !remoteObj.deleted) {
+ obj = 'trashed';
+ }
+ else if (!obj.deleted && remoteObj.deleted) {
+ remoteObj = 'trashed';
+ }
+ */
+ reconcile = true;
+ break;
- toReconcile.push([
- obj,
- remoteObj
- ]);
+ case 'collection':
+ var changed = _mergeCollection(obj, remoteObj);
+ if (!changed) {
+ syncSession.removeFromUpdated(obj);
+ }
+ continue;
- continue;
+ case 'tag':
+ var changed = _mergeTag(obj, remoteObj);
+ if (!changed) {
+ syncSession.removeFromUpdated(obj);
+ }
+ continue;
}
+
+ if (!reconcile) {
+ Zotero.debug(obj);
+ Zotero.debug(remoteObj);
+ var msg = "Reconciliation unimplemented for " + types;
+ alert(msg);
+ throw(msg);
+ }
+
+ // TODO: order reconcile by parent/child?
+
+ toReconcile.push([
+ obj,
+ remoteObj
+ ]);
+
+ continue;
}
- else {
- Zotero.debug("Local " + type + " has not changed", 4);
- }
-
- // Overwrite local below
}
-
- // Key mismatch -- different objects with same id,
- // so change id of local object
else {
- Zotero.debug("Local " + type + " " + xmlNode.@id + " differs from remote", 4);
-
- isNewObject = true;
-
- var oldID = parseInt(xmlNode.@id);
- var newID = Zotero.ID.get(types, true);
-
- Zotero.debug("Changing " + type + " " + oldID + " id to " + newID);
-
- // Save changed object now to update other linked objects
- obj[type + 'ID'] = newID;
- obj.save();
-
- // Update id in local updates array
- //
- // Object might not appear in local update array if server
- // data was cleared and synched from another client
- var index = syncSession.uploadIDs.updated[types].indexOf(oldID);
- if (index != -1) {
- syncSession.uploadIDs.updated[types][index] = newID;
- }
-
- // Add items linked to creators to updated array,
- // since their timestamps will be set to the
- // transaction timestamp
- //
- // Note: Don't need to change collection children or
- // related items, since they're stored as objects
- if (type == 'creator') {
- var linkedItems = obj.getLinkedItems();
- if (linkedItems) {
- syncSession.addToUpdated('item', linkedItems);
- }
- }
-
- syncSession.uploadIDs.changed[types][oldID] = {
- oldID: oldID,
- newID: newID
- };
-
- obj = null;
+ Zotero.debug("Local " + type + " has not changed", 4);
}
+
+ // Overwrite local below
}
// Object doesn't exist locally
else {
isNewObject = true;
- Zotero.debug(syncSession.uploadIDs.deleted);
-
// Check if object has been deleted locally
- for each(var key in syncSession.uploadIDs.deleted[types]) {
- if (key != xmlNode.@key.toString()) {
- continue;
- }
-
+ var fakeObj = {
+ objectType: type,
+ libraryID: libraryID,
+ key: key
+ };
+
+ if (syncSession.objectInDeleted(fakeObj)) {
// TODO: non-merged items
switch (type) {
@@ -1885,7 +2015,7 @@ Zotero.Sync.Server.Data = new function() {
// Auto-restore locally deleted tags that have
// changed remotely
case 'tag':
- syncSession.removeFromDeleted(type, key);
+ syncSession.removeFromDeleted(fakeObj);
var msg = _generateAutoChangeMessage(
type, null, xmlNode.@name.toString()
);
@@ -1897,25 +2027,13 @@ Zotero.Sync.Server.Data = new function() {
throw ('Delete reconciliation unimplemented for ' + types);
}
}
-
- // If key already exists on a different item, change local key
- var oldKey = xmlNode.@key.toString();
- var keyObj = Zotero[Types].getByKey(oldKey);
- if (keyObj) {
- var newKey = Zotero.ID.getKey();
- Zotero.debug("Changing key of local " + type + " " + keyObj.id
- + " from '" + oldKey + "' to '" + newKey + "'", 2);
- keyObj.key = newKey;
- keyObj.save();
- syncSession.addToUpdated(type, keyObj.id);
- }
}
// Temporarily remove and store related items that don't yet exist
if (type == 'item') {
var missing = Zotero.Sync.Server.Data.removeMissingRelatedItems(xmlNode);
if (missing.length) {
- relatedItemsStore[xmlNode.@id] = missing;
+ relatedItemsStore[objLibraryKeyHash] = missing;
}
}
@@ -1926,10 +2044,12 @@ Zotero.Sync.Server.Data = new function() {
// If a local tag matches the name of a different remote tag,
// delete the local tag and add items linked to it to the
// matching remote tag
+ //
+ // DEBUG: why use xmlNode?
var tagName = xmlNode.@name.toString();
var tagType = xmlNode.@type.toString()
? parseInt(xmlNode.@type) : 0;
- var linkedItems = _deleteConflictingTag(syncSession, tagName, tagType);
+ var linkedItems = _deleteConflictingTag(syncSession, tagName, tagType, obj.libraryID);
if (linkedItems) {
var mod = false;
for each(var id in linkedItems) {
@@ -1940,7 +2060,12 @@ Zotero.Sync.Server.Data = new function() {
}
if (mod) {
obj.dateModified = Zotero.DB.transactionDateTime;
- syncSession.addToUpdated('tag', parseInt(xmlNode.@id));
+ Zotero.debug('d');
+ syncSession.addToUpdated({
+ objectType: 'tag',
+ libraryID: obj.libraryID,
+ key: xmlNode.@key
+ });
}
}
}
@@ -1957,9 +2082,6 @@ Zotero.Sync.Server.Data = new function() {
toSave.push(obj);
}
- // Don't use assigned-but-unsaved ids for new ids
- Zotero.ID.skip(types, obj.id);
-
if (type == 'item') {
// Make sure none of the item's creators are marked as
// deleted, which could happen if a creator was deleted
@@ -1968,7 +2090,7 @@ Zotero.Sync.Server.Data = new function() {
if (obj.isRegularItem()) {
var creators = obj.getCreators();
for each(var creator in creators) {
- syncSession.removeFromDeleted('creator', creator.ref.key);
+ syncSession.removeFromDeleted(creator.ref);
}
}
else if (obj.isAttachment() &&
@@ -1985,7 +2107,7 @@ Zotero.Sync.Server.Data = new function() {
else {
var mtime = xmlNode.@storageModTime.toString();
if (mtime) {
- itemStorageModTimes[obj.id] = parseInt(mtime);
+ itemStorageModTimes[obj.key] = parseInt(mtime);
}
}
}
@@ -1996,18 +2118,24 @@ Zotero.Sync.Server.Data = new function() {
//
// Handle remotely deleted objects
//
- if (xml.deleted && xml.deleted[types]) {
+ if (xml.deleted.length() && xml.deleted[types].length()) {
Zotero.debug("Processing remotely deleted " + types);
for each(var xmlNode in xml.deleted[types][type]) {
+ var libraryID = xmlNode.@libraryID.toString();
+ libraryID = libraryID ? parseInt(libraryID) : null;
var key = xmlNode.@key.toString();
- var obj = Zotero[Types].getByKey(key);
+ var obj = Zotero[Types].getByLibraryAndKey(libraryID, key);
// Object can't be found
if (!obj) {
// Since it's already deleted remotely, don't include
// the object in the deleted array if something else
// caused its deletion during the sync
- syncSession.removeFromDeleted(type, xmlNode.@key.toString());
+ syncSession.removeFromDeleted({
+ objectType: type,
+ libraryID: parseInt(xmlNode.@libraryID),
+ key: xmlNode.@key.toString()
+ });
continue;
}
@@ -2067,36 +2195,40 @@ Zotero.Sync.Server.Data = new function() {
);
}
- /*
- if (type == 'collection') {
- // Temporarily remove and store subcollections before saving
- // since referenced collections may not exist yet
- var collections = [];
- for each(var obj in toSave) {
- var colIDs = obj.getChildCollections(true);
- // TODO: use exist(), like related items above
- obj.childCollections = [];
- collections.push({
- obj: obj,
- childCollections: colIDs
- });
- }
- }
- */
-
// Save objects
Zotero.debug('Saving merged ' + types);
- // Save parent items first
- if (type == 'item') {
+
+ if (type == 'collection') {
+ // Save collections recursively from the top down
+ _saveCollections(toSave);
+ }
+ else if (type == 'item') {
+ // Save parent items first
for (var i=0; i<toSave.length; i++) {
- if (!toSave[i].getSource()) {
+ if (!toSave[i].getSourceKey()) {
toSave[i].save();
toSave.splice(i, 1);
i--;
}
}
+
+ // Save the rest
+ for each(var obj in toSave) {
+ obj.save();
+ }
+
+ // Add back related items (which now exist)
+ for (var libraryKeyHash in relatedItemsStore) {
+ var lk = Zotero.Items.parseLibraryKeyHash(libraryKeyHash);
+ var item = Zotero.Items.getByLibraryAndKey(lk.libraryID, lk.key);
+ for each(var relKey in relatedItemsStore[libraryKeyHash]) {
+ var relItem = Zotero.Items.getByLibraryAndKey(lk.libraryID, relKey);
+ item.addRelatedItem(relItem.id);
+ }
+ item.save();
+ }
}
- for each(var obj in toSave) {
+ else if (type == 'tag') {
// Use a special saving mode for tags to avoid an issue that
// occurs if a tag has changed names remotely but another tag
// conflicts with the local version after the first tag has been
@@ -2108,33 +2240,23 @@ Zotero.Sync.Server.Data = new function() {
//
// To replicate, add an item, add a tag, sync both sides,
// rename the tag, add a new one with the old name, and sync.
- var full = type == 'tag';
-
- obj.save(full);
+ for each(var obj in toSave) {
+ obj.save(true);
+ }
}
-
- // Add back related items (which now exist)
- if (type == 'item') {
- for (var itemID in relatedItemsStore) {
- item = Zotero.Items.get(itemID);
- for each(var id in relatedItemsStore[itemID]) {
- item.addRelatedItem(id);
+ else if (type == 'relation') {
+ for each(var obj in toSave) {
+ if (obj.exists()) {
+ continue;
}
- item.save();
+ obj.save();
}
}
- /*
- // Add back subcollections
- else if (type == 'collection') {
- for each(var collection in collections) {
- if (collection.childCollections.length) {
- collection.obj.childCollections = collection.childCollections;
- collection.obj.save();
- }
+ else {
+ for each(var obj in toSave) {
+ obj.save();
}
}
- */
-
// Delete
Zotero.debug('Deleting merged ' + types);
@@ -2157,8 +2279,8 @@ Zotero.Sync.Server.Data = new function() {
// collections so that any deleted items within them don't
// update them, which would trigger erroneous conflicts
var collections = [];
- for each(var colKey in deletedCollectionKeys) {
- var col = Zotero.Collections.getByKey(colKey);
+ for each(var col in deletedCollectionKeys) {
+ col = Zotero.Collections.getByLibrayAndKey(col.libraryID, col.key);
// If collection never existed on this side
if (!col) {
continue;
@@ -2182,7 +2304,7 @@ Zotero.Sync.Server.Data = new function() {
for each(var col in collections) {
col.unlockDateModified();
}
- collection = null;
+ collections = null;
}
else {
Zotero.Sync.EventListener.ignoreDeletions(type, toDelete);
@@ -2195,19 +2317,24 @@ Zotero.Sync.Server.Data = new function() {
// if they've been updated elsewhere and mark for download if so
if (type == 'item') {
var ids = [];
- for (var id in itemStorageModTimes) {
- ids.push(id);
+ var modTimes = {};
+ for (var key in itemStorageModTimes) {
+ var item = Zotero.Items.getByLibraryAndKey(null, key);
+ ids.push(item.id);
+ modTimes[item.id] = itemStorageModTimes[key];
}
if (ids.length > 0) {
- Zotero.Sync.Storage.checkForUpdatedFiles(ids, itemStorageModTimes);
+ Zotero.Sync.Storage.checkForUpdatedFiles(ids, modTimes);
}
}
}
var xmlstr = Zotero.Sync.Server.Data.buildUploadXML(syncSession);
- //Zotero.debug(xmlstr);
- //throw ('break');
+ if (Zotero.Prefs.get('sync.debugBreak')) {
+ Zotero.debug(xmlstr);
+ throw ('break');
+ }
Zotero.DB.commitTransaction();
@@ -2219,7 +2346,8 @@ Zotero.Sync.Server.Data = new function() {
* @param {Zotero.Sync.Server.Session} syncSession
*/
this.buildUploadXML = function (syncSession) {
- var ids = syncSession.uploadIDs;
+ //Zotero.debug(syncSession);
+ var keys = syncSession.uploadKeys;
var xml = <data/>
@@ -2234,36 +2362,29 @@ Zotero.Sync.Server.Data = new function() {
var type = Type.toLowerCase(); // 'item'
var types = Types.toLowerCase(); // 'items'
- if (!ids.updated[types] || !ids.updated[types].length) {
- continue;
- }
-
Zotero.debug("Processing locally changed " + types);
- switch (type) {
- // Items.get() can take multiple ids,
- // so we handle them differently
- case 'item':
- var objs = Zotero[Types].get(ids.updated[types]);
- xml.items = <items/>;
- for each(var obj in objs) {
- //xml[types][type] += this[type + 'ToXML'](obj, syncSession);
- xml[types].appendChild(this[type + 'ToXML'](obj, syncSession));
+ var elementCreated = false;
+ var libraryID, key;
+ for (var libraryID in keys.updated[types]) {
+ for (var key in keys.updated[types][libraryID]) {
+ if (!elementCreated) {
+ xml[types] = new XML("<" + types + "/>");
+ elementCreated = true;
}
- break;
- default:
- xml[types] = new XML("<" + types + "/>");
- for each(var id in ids.updated[types]) {
- var obj = Zotero[Types].get(id);
- //xml[types][type] += this[type + 'ToXML'](obj);
+ var obj = Zotero[Types].getByLibraryAndKey(parseInt(libraryID), key);
+ if (type == 'item') {
+ // itemToXML needs the sync session
+ xml.items.appendChild(this.itemToXML(obj, syncSession));
+ }
+ else {
xml[types].appendChild(this[type + 'ToXML'](obj));
}
+ }
}
}
- // TODO: handle changed ids
-
// Deletions
for each(var syncObject in Zotero.Sync.syncObjects) {
var Type = syncObject.singular; // 'Item'
@@ -2271,16 +2392,21 @@ Zotero.Sync.Server.Data = new function() {
var type = Type.toLowerCase(); // 'item'
var types = Types.toLowerCase(); // 'items'
- if (!ids.deleted[types] || !ids.deleted[types].length) {
- continue;
- }
-
Zotero.debug('Processing locally deleted ' + types);
- for each(var key in ids.deleted[types]) {
- var deletexml = new XML('<' + type + '/>');
- deletexml.@key = key;
- xml.deleted[types][type] += deletexml;
+ var elementCreated = false;
+ var libraryID, key;
+ for (var libraryID in keys.deleted[types]) {
+ for (var key in keys.deleted[types][libraryID]) {
+ if (!elementCreated) {
+ xml.deleted[types] = new XML("<" + types + "/>");
+ elementCreated = true;
+ }
+ var deletexml = new XML('<' + type + '/>');
+ deletexml.@libraryID = parseInt(libraryID) ? parseInt(libraryID) : Zotero.libraryID;
+ deletexml.@key = key;
+ xml.deleted[types].appendChild(deletexml);
+ }
}
}
@@ -2548,6 +2674,7 @@ Zotero.Sync.Server.Data = new function() {
* Process the results of conflict resolution
*/
function _processMergeData(syncSession, type, data, toSave, toDelete, relatedItems) {
+ var Types = Zotero.Sync.syncObjects[type].plural;
var types = Zotero.Sync.syncObjects[type].plural.toLowerCase();
for each(var obj in data) {
@@ -2559,30 +2686,27 @@ Zotero.Sync.Server.Data = new function() {
if (obj.left != 'deleted') {
toDelete.push(obj.id);
- if (relatedItems[obj.id]) {
- delete relatedItems[obj.id];
+ var libraryKeyHash = Zotero[Types].getLibraryKeyHash(obj.ref);
+ if (relatedItems[libraryKeyHash]) {
+ delete relatedItems[libraryKeyHash];
}
- syncSession.addToDeleted(type, obj.left.key);
+ syncSession.addToDeleted(obj.left);
}
continue;
}
toSave.push(obj.ref);
- // Don't use assigned-but-unsaved ids for new ids
- Zotero.ID.skip(types, obj.id);
-
// Item had been deleted locally, so remove from
// deleted array
if (obj.left == 'deleted') {
- syncSession.removeFromDeleted(type, obj.ref.key);
+ syncSession.removeFromDeleted(obj.ref);
}
// TODO: only upload if the local item was chosen
// or remote item was changed
-
- syncSession.addToUpdated(type, obj.id);
+ syncSession.addToUpdated(obj.ref);
}
}
@@ -2597,13 +2721,17 @@ Zotero.Sync.Server.Data = new function() {
var xml = <item/>;
var item = item.serialize();
+ xml.@libraryID = item.primary.libraryID ? item.primary.libraryID : Zotero.libraryID;
+ xml.@key = item.primary.key;
+
// Primary fields
for (var field in item.primary) {
switch (field) {
case 'itemID':
- var attr = 'id';
- break;
-
+ case 'libraryID':
+ case 'key':
+ continue;
+
default:
var attr = field;
}
@@ -2627,7 +2755,8 @@ Zotero.Sync.Server.Data = new function() {
if (item.primary.itemType == 'note' || item.primary.itemType == 'attachment') {
if (item.sourceItemID) {
- xml.@sourceItemID = item.sourceItemID;
+ var sourceItem = Zotero.Items.get(item.sourceItemID);
+ xml.@sourceItem = sourceItem.key;
}
}
@@ -2666,15 +2795,24 @@ Zotero.Sync.Server.Data = new function() {
// Creators
for (var index in item.creators) {
var newCreator = <creator/>;
- var creatorID = item.creators[index].creatorID;
- newCreator.@id = creatorID;
+ var libraryID = item.creators[index].libraryID ? item.creators[index].libraryID : Zotero.libraryID;
+ var key = item.creators[index].key;
+ if (!key) {
+ throw ("Creator key not set for item in Zotero.Sync.Server.sync()");
+ }
+ newCreator.@libraryID = libraryID;
+ newCreator.@key = key;
newCreator.@creatorType = item.creators[index].creatorType;
newCreator.@index = index;
// Add creator XML as glue if not already included in sync session
- if (syncSession &&
- syncSession.uploadIDs.updated.creators.indexOf(creatorID) == -1) {
- var creator = Zotero.Creators.get(creatorID);
+ var fakeObj = {
+ objectType: 'creator',
+ libraryID: libraryID,
+ key: key
+ };
+ if (syncSession && syncSession.objectInUpdated(fakeObj)) {
+ var creator = Zotero.Creators.getByLibraryAndKey(libraryID, key);
var creatorXML = Zotero.Sync.Server.Data.creatorToXML(creator);
newCreator.creator = creatorXML;
}
@@ -2683,8 +2821,16 @@ Zotero.Sync.Server.Data = new function() {
}
// Related items
- if (item.related.length) {
- xml.related = item.related.join(' ');
+ var related = item.related;
+ if (related.length) {
+ related = Zotero.Items.get(related);
+ var keys = [];
+ for each(var item in related) {
+ keys.push(item.key);
+ }
+ if (keys.length) {
+ xml.related = keys.join(' ');
+ }
}
return xml;
@@ -2700,18 +2846,7 @@ Zotero.Sync.Server.Data = new function() {
*/
function xmlToItem(xmlItem, item, skipPrimary) {
if (!item) {
- if (skipPrimary) {
- item = new Zotero.Item;
- }
- else {
- item = new Zotero.Item(parseInt(xmlItem.@id));
- /*
- if (item.exists()) {
- _error("Item specified in XML node already exists "
- + "in Zotero.Sync.Server.Data.xmlToItem()");
- }
- */
- }
+ item = new Zotero.Item;
}
else if (skipPrimary) {
throw ("Cannot use skipPrimary with existing item in "
@@ -2720,14 +2855,15 @@ Zotero.Sync.Server.Data = new function() {
// TODO: add custom item types
- var data = {
- itemTypeID: Zotero.ItemTypes.getID(xmlItem.@itemType.toString())
- };
+ var data = {};
if (!skipPrimary) {
+ var libraryID = xmlItem.@libraryID.toString();
+ data.libraryID = libraryID ? parseInt(libraryID) : null;
+ data.key = xmlItem.@key.toString();
data.dateAdded = xmlItem.@dateAdded.toString();
data.dateModified = xmlItem.@dateModified.toString();
- data.key = xmlItem.@key.toString();
}
+ data.itemTypeID = Zotero.ItemTypes.getID(xmlItem.@itemType.toString());
var changedFields = {};
@@ -2767,19 +2903,18 @@ Zotero.Sync.Server.Data = new function() {
throw ('No creator in position ' + i);
}
- var creatorID = parseInt(creator.@id);
- var creatorObj = Zotero.Creators.get(creatorID);
+ var creatorObj = Zotero.Creators.getByLibraryAndKey(data.libraryID, creator.@key.toString());
// If creator doesn't exist locally (e.g., if it was deleted locally
// and appears in a new/modified item remotely), get it from within
// the item's creator block, where a copy should be provided
if (!creatorObj) {
if (creator.creator.length() == 0) {
- throw ("Data for missing local creator " + creatorID
+ throw ("Data for missing local creator "
+ + data.libraryID + "/" + creator.@key.toString()
+ " not provided in Zotero.Sync.Server.Data.xmlToItem()");
}
- var creatorObj =
- Zotero.Sync.Server.Data.xmlToCreator(creator.creator);
- if (creatorObj.id != creatorID) {
+ var creatorObj = Zotero.Sync.Server.Data.xmlToCreator(creator.creator);
+ if (creatorObj.id != parseInt(creator.@id)) {
throw ("Creator id " + creatorObj.id + " does not match "
+ "item creator in Zotero.Sync.Server.Data.xmlToItem()");
}
@@ -2802,8 +2937,8 @@ Zotero.Sync.Server.Data = new function() {
// Both notes and attachments might have parents and notes
if (item.isNote() || item.isAttachment()) {
- var sourceItemID = parseInt(xmlItem.@sourceItemID);
- item.setSource(sourceItemID ? sourceItemID : false);
+ var sourceItemKey = xmlItem.@sourceItem.toString();
+ item.setSourceKey(sourceItemKey ? sourceItemKey : null);
item.setNote(xmlItem.note.toString());
}
@@ -2819,39 +2954,53 @@ Zotero.Sync.Server.Data = new function() {
// Related items
var related = xmlItem.related.toString();
- item.relatedItems = related ? related.split(' ') : [];
+ var relatedIDs = [];
+ if (related) {
+ related = related.split(' ');
+ for each(var key in related) {
+ var relItem = Zotero.Items.getByLibraryAndKey(item.libraryID, key);
+ if (!relItem) {
+ throw ("Related item " + item.libraryID + "/" + key
+ + " doesn't exist in Zotero.Sync.Server.Data.xmlToItem()");
+ }
+ relatedIDs.push(relItem.id);
+ }
+ }
+ item.relatedItems = relatedIDs;
return item;
}
function removeMissingRelatedItems(xmlNode) {
+ var libraryID = parseInt(xmlNode.@libraryID);
+ var exist = [];
var missing = [];
- var related = xmlNode.related.toString();
- var relIDs = related ? related.split(' ') : [];
- if (relIDs.length) {
- var exist = Zotero.Items.exist(relIDs);
- for each(var id in relIDs) {
- if (exist.indexOf(id) == -1) {
- missing.push(id);
- }
+ var relKeys = xmlNode.related.toString();
+ relKeys = relKeys ? relKeys.split(' ') : [];
+ for each(var relKey in relKeys) {
+ if (Zotero.Items.getByLibraryAndKey(libraryID, relKey)) {
+ exist.push(relKey);
+ }
+ else {
+ missing.push(relKey);
}
- xmlNode.related = exist.join(' ');
}
+ xmlNode.related = exist.join(' ');
return missing;
}
function collectionToXML(collection) {
var xml = <collection/>;
-
- xml.@id = collection.id;
+ xml.@libraryID = collection.libraryID ? collection.libraryID : Zotero.libraryID;
+ xml.@key = collection.key;
xml.@name = _xmlize(collection.name);
xml.@dateAdded = collection.dateAdded;
xml.@dateModified = collection.dateModified;
- xml.@key = collection.key;
if (collection.parent) {
- xml.@parent = collection.parent;
+ var parentCol = Zotero.Collections.get(collection.parent);
+ xml.@parent = parentCol.key;
}
var children = collection.getChildren();
@@ -2866,7 +3015,7 @@ Zotero.Sync.Server.Data = new function() {
}
else */if (child.type == 'item') {
xml.items = xml.items ?
- xml.items + ' ' + child.id : child.id;
+ xml.items + ' ' + child.key : child.key;
}
}
/*
@@ -2892,33 +3041,30 @@ Zotero.Sync.Server.Data = new function() {
*/
function xmlToCollection(xmlCollection, collection, skipPrimary) {
if (!collection) {
- if (skipPrimary) {
- collection = new Zotero.Collection(null);
- }
- else {
- collection = new Zotero.Collection(parseInt(xmlCollection.@id));
- /*
- if (collection.exists()) {
- throw ("Collection specified in XML node already exists "
- + "in Zotero.Sync.Server.Data.xmlToCollection()");
- }
- */
- }
+ collection = new Zotero.Collection;
}
else if (skipPrimary) {
throw ("Cannot use skipPrimary with existing collection in "
+ "Zotero.Sync.Server.Data.xmlToCollection()");
}
- collection.name = xmlCollection.@name.toString();
if (!skipPrimary) {
- collection.parent = xmlCollection.@parent.toString() ?
- parseInt(xmlCollection.@parent) : null;
+ var libraryID = xmlCollection.@libraryID.toString();
+ collection.libraryID = libraryID ? parseInt(libraryID) : null;
+ collection.key = xmlCollection.@key.toString();
+ var parentKey = xmlCollection.@parent.toString();
+ if (parentKey) {
+ collection.parentKey = parentKey;
+ }
+ else {
+ collection.parent = false;
+ }
collection.dateAdded = xmlCollection.@dateAdded.toString();
collection.dateModified = xmlCollection.@dateModified.toString();
- collection.key = xmlCollection.@key.toString();
}
+ collection.name = xmlCollection.@name.toString();
+
/*
// Subcollections
var str = xmlCollection.collections.toString();
@@ -2926,33 +3072,69 @@ Zotero.Sync.Server.Data = new function() {
*/
// Child items
- var str = xmlCollection.items.toString();
- collection.childItems = str == '' ? [] : str.split(' ');
+ var childItems = xmlCollection.items.toString();
+ childItems = childItems ? childItems.split(' ') : []
+ var childItemIDs = [];
+ for each(var key in childItems) {
+ var childItem = Zotero.Items.getByLibraryAndKey(collection.libraryID, key);
+ childItemIDs.push(childItem.id);
+ }
+ collection.childItems = childItemIDs;
return collection;
}
/**
+ * Recursively save collections from the top down
+ */
+ function _saveCollections(collections) {
+ var unsaved = [];
+
+ var parentKey, parentCollection;
+
+ for each(var collection in collections) {
+ parentKey = collection.parentKey;
+ // Top-level collection, so save
+ if (!parentKey) {
+ collection.save();
+ continue;
+ }
+ parentCollection = Zotero.Collections.getByLibraryAndKey(
+ collection.libraryID, parentKey
+ );
+ // Parent collection exists, so save
+ if (parentCollection) {
+ collection.save();
+ continue;
+ }
+
+ // Add to unsaved list
+ unsaved.push(collection);
+ continue;
+ }
+
+ if (unsaved.length) {
+ _saveCollections(unsaved);
+ }
+ }
+
+
+
+ /**
* Converts a Zotero.Creator object to an E4X <creator> object
*/
function creatorToXML(creator) {
var xml = <creator/>;
- var creator = creator.serialize();
- for (var field in creator.primary) {
- switch (field) {
- case 'creatorID':
- var attr = 'id';
- break;
-
- default:
- var attr = field;
- }
- xml['@' + attr] = creator.primary[field];
- }
+
+ xml.@libraryID = creator.libraryID ? creator.libraryID : Zotero.libraryID;
+ xml.@key = creator.key;
+ xml.@dateAdded = creator.dateAdded;
+ xml.@dateModified = creator.dateModified;
var allowEmpty = ['firstName', 'lastName', 'name'];
+ var creator = creator.serialize();
for (var field in creator.fields) {
if (!creator.fields[field] && allowEmpty.indexOf(field) == -1) {
continue;
@@ -2982,45 +3164,32 @@ Zotero.Sync.Server.Data = new function() {
*/
function xmlToCreator(xmlCreator, creator, skipPrimary) {
if (!creator) {
- if (skipPrimary) {
- creator = new Zotero.Creator;
- }
- else {
- creator = new Zotero.Creator(parseInt(xmlCreator.@id));
- /*
- if (creator.exists()) {
- throw ("Creator specified in XML node already exists "
- + "in Zotero.Sync.Server.Data.xmlToCreator()");
- }
- */
- }
+ creator = new Zotero.Creator;
}
else if (skipPrimary) {
throw ("Cannot use skipPrimary with existing creator in "
+ "Zotero.Sync.Server.Data.xmlToCreator()");
}
- var data = {
- birthYear: xmlCreator.birthYear.toString()
- };
if (!skipPrimary) {
- data.dateAdded = xmlCreator.@dateAdded.toString();
- data.dateModified = xmlCreator.@dateModified.toString();
- data.key = xmlCreator.@key.toString();
+ var libraryID = xmlCreator.@libraryID.toString();
+ creator.libraryID = libraryID ? parseInt(libraryID) : null;
+ creator.key = xmlCreator.@key.toString();
+ creator.dateAdded = xmlCreator.@dateAdded.toString();
+ creator.dateModified = xmlCreator.@dateModified.toString();
}
if (xmlCreator.fieldMode == 1) {
- data.firstName = '';
- data.lastName = xmlCreator.name.toString();
- data.fieldMode = 1;
+ creator.firstName = '';
+ creator.lastName = xmlCreator.name.toString();
+ creator.fieldMode = 1;
}
else {
- data.firstName = xmlCreator.firstName.toString();
- data.lastName = xmlCreator.lastName.toString();
- data.fieldMode = 0;
+ creator.firstName = xmlCreator.firstName.toString();
+ creator.lastName = xmlCreator.lastName.toString();
+ creator.fieldMode = 0;
}
-
- creator.setFields(data);
+ creator.birthYear = xmlCreator.birthYear.toString();
return creator;
}
@@ -3028,12 +3197,11 @@ Zotero.Sync.Server.Data = new function() {
function searchToXML(search) {
var xml = <search/>;
-
- xml.@id = search.id;
+ xml.@libraryID = search.libraryID ? search.libraryID : Zotero.libraryID;
+ xml.@key = search.key;
xml.@name = _xmlize(search.name);
xml.@dateAdded = search.dateAdded;
xml.@dateModified = search.dateModified;
- xml.@key = search.key;
var conditions = search.getSearchConditions();
if (conditions) {
@@ -3067,31 +3235,23 @@ Zotero.Sync.Server.Data = new function() {
*/
function xmlToSearch(xmlSearch, search, skipPrimary) {
if (!search) {
- if (skipPrimary) {
- search = new Zotero.Search(null);
- }
- else {
- search = new Zotero.Search(parseInt(xmlSearch.@id));
- /*
- if (search.exists()) {
- throw ("Search specified in XML node already exists "
- + "in Zotero.Sync.Server.Data.xmlToSearch()");
- }
- */
- }
+ search = new Zotero.Search;
}
else if (skipPrimary) {
throw ("Cannot use new id with existing search in "
+ "Zotero.Sync.Server.Data.xmlToSearch()");
}
- search.name = xmlSearch.@name.toString();
if (!skipPrimary) {
+ var libraryID = xmlSearch.@libraryID.toString();
+ search.libraryID = libraryID ? parseInt(libraryID) : null;
+ search.key = xmlSearch.@key.toString();
search.dateAdded = xmlSearch.@dateAdded.toString();
search.dateModified = xmlSearch.@dateModified.toString();
- search.key = xmlSearch.@key.toString();
}
+ search.name = xmlSearch.@name.toString();
+
var conditionID = -1;
// Search conditions
@@ -3135,18 +3295,21 @@ Zotero.Sync.Server.Data = new function() {
function tagToXML(tag) {
var xml = <tag/>;
-
- xml.@id = tag.id;
+ xml.@libraryID = tag.libraryID ? tag.libraryID : Zotero.libraryID;
+ xml.@key = tag.key;
xml.@name = _xmlize(tag.name);
if (tag.type) {
xml.@type = tag.type;
}
xml.@dateAdded = tag.dateAdded;
xml.@dateModified = tag.dateModified;
- xml.@key = tag.key;
- var linkedItems = tag.getLinkedItems(true);
+ var linkedItems = tag.getLinkedItems();
if (linkedItems) {
- xml.items = linkedItems.join(' ');
+ var linkedItemKeys = [];
+ for each(var linkedItem in linkedItems) {
+ linkedItemKeys.push(linkedItem.key);
+ }
+ xml.items = linkedItemKeys.join(' ');
}
return xml;
}
@@ -3161,34 +3324,39 @@ Zotero.Sync.Server.Data = new function() {
*/
function xmlToTag(xmlTag, tag, skipPrimary) {
if (!tag) {
- if (skipPrimary) {
- tag = new Zotero.Tag;
- }
- else {
- tag = new Zotero.Tag(parseInt(xmlTag.@id));
- /*
- if (tag.exists()) {
- throw ("Tag specified in XML node already exists "
- + "in Zotero.Sync.Server.Data.xmlToTag()");
- }
- */
- }
+ tag = new Zotero.Tag;
}
else if (skipPrimary) {
throw ("Cannot use new id with existing tag in "
+ "Zotero.Sync.Server.Data.xmlToTag()");
}
- tag.name = xmlTag.@name.toString();
- tag.type = xmlTag.@type.toString() ? parseInt(xmlTag.@type) : 0;
if (!skipPrimary) {
+ var libraryID = xmlTag.@libraryID.toString();
+ tag.libraryID = libraryID ? parseInt(libraryID) : null;
+ tag.key = xmlTag.@key.toString();
tag.dateAdded = xmlTag.@dateAdded.toString();
tag.dateModified = xmlTag.@dateModified.toString();
- tag.key = xmlTag.@key.toString();
}
- var str = xmlTag.items ? xmlTag.items.toString() : false;
- tag.linkedItems = str ? str.split(' ') : [];
+ tag.name = xmlTag.@name.toString();
+ tag.type = xmlTag.@type.toString() ? parseInt(xmlTag.@type) : 0;
+
+ var keys = xmlTag.items.toString() ? xmlTag.items.toString().split(' ') : false;
+ if (keys) {
+ var ids = [];
+ for each(var key in keys) {
+ var item = Zotero.Items.getByLibraryAndKey(tag.libraryID, key);
+ if (!item) {
+ throw ("Linked item " + key + " doesn't exist in Zotero.Sync.Server.Data.xmlToTag()");
+ }
+ ids.push(item.id);
+ }
+ }
+ else {
+ var ids = [];
+ }
+ tag.linkedItems = ids;
return tag;
}
@@ -3201,8 +3369,8 @@ Zotero.Sync.Server.Data = new function() {
* deleted tag, or FALSE if no
* matching tag found
*/
- function _deleteConflictingTag(syncSession, name, type) {
- var tagID = Zotero.Tags.getID(name, type);
+ function _deleteConflictingTag(syncSession, name, type, libraryID) {
+ var tagID = Zotero.Tags.getID(name, type, libraryID);
if (tagID) {
Zotero.debug("Deleting conflicting local tag " + tagID);
var tag = Zotero.Tags.get(tagID);
@@ -3210,8 +3378,8 @@ Zotero.Sync.Server.Data = new function() {
Zotero.Tags.erase(tagID);
Zotero.Tags.purge();
- syncSession.removeFromUpdated('tag', tagID);
- //syncSession.addToDeleted('tag', tag.key);
+ syncSession.removeFromUpdated(tag);
+ //syncSession.addToDeleted(tag);
return linkedItems ? linkedItems : [];
}
@@ -3220,6 +3388,57 @@ Zotero.Sync.Server.Data = new function() {
}
+ /**
+ * Convert E4X <group> object into an unsaved Zotero.Group
+ *
+ * @param object xmlGroup E4X XML node with group data
+ * @param object group (Optional) Existing Zotero.Group to update
+ */
+ this.xmlToGroup = function (xmlGroup, group) {
+ if (!group) {
+ group = new Zotero.Group;
+ }
+
+ Zotero.debug(xmlGroup.toXMLString());
+
+ group.id = parseInt(xmlGroup.@id);
+ group.libraryID = parseInt(xmlGroup.@libraryID);
+ group.name = xmlGroup.@name.toString();
+ group.editable = !!parseInt(xmlGroup.@editable);
+ group.filesEditable = !!parseInt(xmlGroup.@filesEditable);
+ group.description = xmlGroup.description.toString();
+
+ /*
+ var keys = xmlGroup.items.toString() ? xmlGroup.items.toString().split(' ') : false;
+ if (keys) {
+ var ids = [];
+ for each(var key in keys) {
+ var item = Zotero.Items.getByLibraryAndKey(group.libraryID, key);
+ if (!item) {
+ throw ("Linked item " + key + " doesn't exist in Zotero.Sync.Server.Data.xmlToGroup()");
+ }
+ ids.push(item.id);
+ }
+ }
+ else {
+ var ids = [];
+ }
+ group.linkedItems = ids;
+ */
+
+ return group;
+ }
+
+
+ this.relationToXML = function (relation) {
+ return relation.toXML();
+ }
+
+ this.xmlToRelation = function (xmlRelation) {
+ return Zotero.Relations.xmlToRelation(xmlRelation);
+ }
+
+
function _xmlize(str) {
return str.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\ud800-\udfff\ufffe\uffff]/g, '\u2B1A');
}
diff --git a/chrome/content/zotero/xpcom/translate.js b/chrome/content/zotero/xpcom/translate.js
@@ -265,8 +265,8 @@ Zotero.Translator.prototype.logError = function(message, type, line, lineNumber,
* setItems). setting items disables export of collections.
* path - the path to the target; for web, this is the same as location
* string - the string content to be used as a file.
- * saveItem - whether new items should be saved to the database. defaults to
- * true; set using second argument of constructor.
+ * libraryID - libraryID (e.g., of a group) of saved database items. null for local items,
+ * or false not to save. defaults to null; set using second argument of constructor.
* newItems - items created when translate() was called
* newCollections - collections created when translate() was called
*
@@ -305,7 +305,11 @@ Zotero.Translator.prototype.logError = function(message, type, line, lineNumber,
*
* output - export output (if no location has been specified)
*/
-Zotero.Translate = function(type, saveItem, saveAttachments) {
+Zotero.Translate = function(type) {
+ if (arguments.length > 1) {
+ throw ("Zotero.Translate only takes one parameter");
+ }
+
this.type = type;
// import = 0001 = 1
@@ -335,9 +339,6 @@ Zotero.Translate = function(type, saveItem, saveAttachments) {
}
this._numericTypes = this._numericTypes.substr(1);
- this.saveItem = !(saveItem === false);
- this.saveAttachments = !(saveAttachments === false);
-
this._handlers = new Array();
this._streams = new Array();
}
@@ -606,10 +607,15 @@ Zotero.Translate.prototype._loadTranslator = function() {
return true;
}
-/*
+/**
* does the actual translation
+ *
+ * @param {NULL|Integer|FALSE} [libraryID=null] Library in which to save items,
+ * or NULL for default library;
+ * if FALSE, don't save items
+ * @param {Boolean} [saveAttachments=true]
*/
-Zotero.Translate.prototype.translate = function() {
+Zotero.Translate.prototype.translate = function(libraryID, saveAttachments) {
/*
* initialize properties
*/
@@ -629,6 +635,24 @@ Zotero.Translate.prototype.translate = function() {
throw("cannot translate: no location specified");
}
+ this.libraryID = (libraryID == undefined) ? null : libraryID;
+ this.saveAttachments = !(saveAttachments === false);
+ this.saveFiles = this.saveAttachments;
+
+ // If group filesEditable==false, don't save attachments
+ if (this.libraryID) {
+ var type = Zotero.Libraries.getType(this.libraryID);
+ switch (type) {
+ case 'group':
+ var groupID = Zotero.Groups.getGroupIDFromLibraryID(this.libraryID);
+ var group = Zotero.Groups.get(groupID);
+ if (!group.filesEditable) {
+ this.saveFiles = false;
+ }
+ break;
+ }
+ }
+
// erroring should end
this.error = this._translationComplete;
@@ -750,7 +774,7 @@ Zotero.Translate.prototype._generateSandbox = function() {
// for loading other translators and accessing their methods
this._sandbox.Zotero.loadTranslator = function(type) {
- var translation = new Zotero.Translate(type, false);
+ var translation = new Zotero.Translate(type);
translation._parentTranslator = me;
if(type == "export" && (this.type == "web" || this.type == "search")) {
@@ -781,7 +805,7 @@ Zotero.Translate.prototype._generateSandbox = function() {
}
}
- return translation.translate()
+ return translation.translate(false);
};
safeTranslator.getTranslatorObject = function() {
// load the translator into our sandbox
@@ -1134,7 +1158,7 @@ Zotero.Translate.prototype._itemDone = function(item, attachedTo) {
this._itemsDone = true;
- if(!this.saveItem) { // if we're not supposed to save the item, just
+ if(this.libraryID === false) { // if we're not supposed to save the item, just
// return the item array
// if a parent sandbox exists, use complete() function from that sandbox
@@ -1151,7 +1175,8 @@ Zotero.Translate.prototype._itemDone = function(item, attachedTo) {
var type = (item.itemType ? item.itemType : "webpage");
if(type == "note") { // handle notes differently
- var newItem = new Zotero.Item(false, 'note');
+ var newItem = new Zotero.Item('note');
+ newItem.libraryID = this.libraryID ? this.libraryID : null;
newItem.setNote(item.note);
var myID = newItem.save();
// re-retrieve the item
@@ -1248,7 +1273,8 @@ Zotero.Translate.prototype._itemDone = function(item, attachedTo) {
}
} else {
var typeID = Zotero.ItemTypes.getID(type);
- var newItem = new Zotero.Item(false, typeID);
+ var newItem = new Zotero.Item(typeID);
+ newItem.libraryID = this.libraryID ? this.libraryID : null;
}
// makes looping through easier
@@ -1280,7 +1306,7 @@ Zotero.Translate.prototype._itemDone = function(item, attachedTo) {
}
// Single-field mode
- if (data[j].fieldMode == 1) {
+ if (data[j].fieldMode && data[j].fieldMode == 1) {
var fields = {
lastName: data[j].lastName,
fieldMode: 1
@@ -1294,14 +1320,19 @@ Zotero.Translate.prototype._itemDone = function(item, attachedTo) {
};
}
+ var creator = null;
var creatorDataID = Zotero.Creators.getDataID(fields);
if(creatorDataID) {
- var linkedCreators = Zotero.Creators.getCreatorsWithData(creatorDataID);
- // TODO: support identical creators via popup? ugh...
- var creatorID = linkedCreators[0];
- var creator = Zotero.Creators.get(creatorID);
- } else {
- var creator = new Zotero.Creator;
+ var linkedCreators = Zotero.Creators.getCreatorsWithData(creatorDataID, this.libraryID);
+ if (linkedCreators) {
+ // TODO: support identical creators via popup? ugh...
+ var creatorID = linkedCreators[0];
+ creator = Zotero.Creators.get(creatorID);
+ }
+ }
+ if(!creator) {
+ creator = new Zotero.Creator;
+ creator.libraryID = this.libraryID ? this.libraryID : null;
creator.setFields(fields);
var creatorID = creator.save();
}
@@ -1375,7 +1406,8 @@ Zotero.Translate.prototype._itemDone = function(item, attachedTo) {
// handle notes
if(item.notes) {
for each(var note in item.notes) {
- var myNote = new Zotero.Item(false, 'note');
+ var myNote = new Zotero.Item('note');
+ myNote.libraryID = this.libraryID ? this.libraryID : null;
myNote.setNote(note.note);
if (myID) {
myNote.setSource(myID);
@@ -1423,9 +1455,9 @@ Zotero.Translate.prototype._itemDone = function(item, attachedTo) {
Zotero.debug("Translate: Error adding attachment "+attachment.url, 2);
}
}
- } else if(attachment.document
+ } else if(this.saveFiles && (attachment.document
|| (attachment.mimeType && attachment.mimeType == "text/html")
- || downloadAssociatedFiles) {
+ || downloadAssociatedFiles)) {
// if snapshot is not explicitly set to false, retrieve snapshot
if(attachment.document) {
@@ -1438,8 +1470,8 @@ Zotero.Translate.prototype._itemDone = function(item, attachedTo) {
}
// Save attachment if snapshot pref enabled or not HTML
// (in which case downloadAssociatedFiles applies)
- } else if(automaticSnapshots || !attachment.mimeType
- || attachment.mimeType != "text/html") {
+ } else if(this.saveFiles && (automaticSnapshots || !attachment.mimeType
+ || attachment.mimeType != "text/html")) {
var mimeType = null;
var title = null;
diff --git a/chrome/content/zotero/xpcom/uri.js b/chrome/content/zotero/xpcom/uri.js
@@ -0,0 +1,112 @@
+Zotero.URI = new function () {
+ var _baseURI = ZOTERO_CONFIG.BASE_URI;
+
+
+ this.getCurrentUserURI = function () {
+ var userID = Zotero.userID;
+ if (userID) {
+ return _baseURI + "users/" + userID;
+ }
+
+ return _baseURI + "users/local/" + Zotero.getLocalUserKey(true);
+ }
+
+
+ this.getLibraryURI = function (libraryID) {
+ var libraryType = Zotero.Libraries.getType(libraryID);
+ switch (libraryType) {
+ case 'group':
+ var id = Zotero.Groups.getGroupIDFromLibraryID(libraryID);
+ break;
+
+ case 'user':
+ throw ("User library ids are not supported in Zotero.URI.getLibraryURI");
+
+ default:
+ throw ("Unsupported library type '" + libraryType + "' in Zotero.URI.getLibraryURI()");
+ }
+ return _baseURI + libraryType + "s/" + id;
+ }
+
+
+ this.getItemURI = function (item) {
+ if (item.libraryID) {
+ var baseURI = this.getLibraryURI(item.libraryID);
+ }
+ else {
+ var baseURI = this.getCurrentUserURI();
+ }
+ return baseURI + "/items/" + item.key;
+ }
+
+
+ this.getGroupsURL = function () {
+ return ZOTERO_CONFIG.WWW_BASE_URL + "groups";
+ }
+
+
+ /**
+ * @param {Zotero.Group} group
+ * @return {String}
+ */
+ this.getGroupURI = function (group, webRoot) {
+ var uri = _baseURI + "groups/" + group.id;
+ if (webRoot) {
+ uri = uri.replace(ZOTERO_CONFIG.BASE_URI, ZOTERO_CONFIG.WWW_BASE_URL);
+ }
+ return uri;
+ }
+
+
+ /**
+ * Convert an item URI into an item
+ *
+ * @param {String} itemURI
+ * @param {Zotero.Item|FALSE}
+ */
+ this.getURIItem = function (itemURI) {
+ var libraryType = null;
+
+ // If this is a local URI, compare to the local user key
+ if (itemURI.match(/\/users\/local\//)) {
+ var currentUserURI = this.getCurrentUserURI() + "/";
+ if (itemURI.indexOf(currentUserURI) == 0) {
+ itemURI = itemURI.substr(currentUserURI.length);
+ var libraryType = 'user';
+ var libraryTypeID = null;
+ }
+ }
+
+ // If not found, try global URI
+ if (!libraryType) {
+ if (itemURI.indexOf(_baseURI) != 0) {
+ throw ("Invalid base URI '" + itemURI + "' in Zotero.URI.getURIItem()");
+ }
+ itemURI = itemURI.substr(_baseURI.length);
+ var typeRE = /^(users|groups)\/([0-9]+)\//;
+ var matches = itemURI.match(typeRE);
+ if (!matches) {
+ throw ("Invalid library URI '" + itemURI + "' in Zotero.URI.getURIItem()");
+ }
+ var libraryType = matches[1].substr(0, matches[1].length-1);
+ var libraryTypeID = matches[2];
+ itemURI = itemURI.replace(typeRE, '');
+ }
+
+ // TODO: itemID-based URI?
+ var matches = itemURI.match(/items\/([A-Z0-9]{8})/);
+ if (!matches) {
+ throw ("Invalid item URI '" + itemURI + "' in Zotero.URI.getURIItem()");
+ }
+ var itemKey = matches[1];
+
+ if (libraryType == 'user') {
+ return Zotero.Items.getByLibraryAndKey(null, itemKey);
+ }
+
+ if (libraryType == 'group') {
+ var libraryID = Zotero.Groups.getLibraryIDFromGroupID(libraryTypeID);
+ return Zotero.Items.getByLibraryAndKey(libraryID, itemKey);
+ }
+ }
+}
diff --git a/chrome/content/zotero/xpcom/zotero.js b/chrome/content/zotero/xpcom/zotero.js
@@ -27,6 +27,8 @@ const ZOTERO_CONFIG = {
REPOSITORY_CHECK_INTERVAL: 86400, // 24 hours
REPOSITORY_RETRY_INTERVAL: 3600, // 1 hour
FIRST_RUN_URL: 'http://www.zotero.org/support/quick_start_guide',
+ BASE_URI: 'http://zotero.org/',
+ WWW_BASE_URL: 'http://www.zotero.org/',
SYNC_URL: 'https://sync.zotero.org/'
};
@@ -62,7 +64,6 @@ var Zotero = new function(){
this.hasValues = hasValues;
this.randomString = randomString;
this.moveToUnique = moveToUnique;
- this.reloadDataObjects = reloadDataObjects;
// Public properties
this.initialized = false;
@@ -77,6 +78,49 @@ var Zotero = new function(){
this.isWin;
this.initialURL; // used by Schema to show the changelog on upgrades
+
+ this.__defineGetter__('userID', function () {
+ var sql = "SELECT value FROM settings WHERE "
+ + "setting='account' AND key='userID'";
+ return Zotero.DB.valueQuery(sql);
+ });
+
+ this.__defineSetter__('userID', function (val) {
+ var sql = "REPLACE INTO settings VALUES ('account', 'userID', ?)";
+ Zotero.DB.query(sql, parseInt(val));
+ });
+
+ this.__defineGetter__('libraryID', function () {
+ var sql = "SELECT value FROM settings WHERE "
+ + "setting='account' AND key='libraryID'";
+ return Zotero.DB.valueQuery(sql);
+ });
+
+ this.__defineSetter__('libraryID', function (val) {
+ var sql = "REPLACE INTO settings VALUES ('account', 'libraryID', ?)";
+ Zotero.DB.query(sql, parseInt(val));
+ });
+
+ this.getLocalUserKey = function (generate) {
+ if (_localUserKey) {
+ return _localUserKey;
+ }
+
+ var sql = "SELECT value FROM settings WHERE "
+ + "setting='account' AND key='localUserKey'";
+ var key = Zotero.DB.valueQuery(sql);
+
+ // Generate a local user key if we don't have a global library id
+ if (!key && generate) {
+ key = Zotero.randomString(8);
+ var sql = "INSERT INTO settings VALUES ('account', 'localUserKey', ?)";
+ Zotero.DB.query(sql, key);
+ }
+ _localUserKey = key;
+ return key;
+ };
+
+
var _startupError;
var _startupErrorHandler;
var _zoteroDirectory = false;
@@ -85,7 +129,7 @@ var Zotero = new function(){
var _debugTime;
var _debugLastTime;
var _localizedStringBundle;
-
+ var _localUserKey;
/*
* Initialize the extension
@@ -841,19 +885,21 @@ var Zotero = new function(){
* values and/or an arbitrary number of individual values
*/
function flattenArguments(args){
+ var isArguments = args.callee && args.length;
+
// Put passed scalar values into an array
- if (typeof args!='object' || args===null){
+ if (args === null || (args.constructor.name != 'Array' && !isArguments)) {
args = [args];
}
- var returns = new Array();
-
+ var returns = [];
for (var i=0; i<args.length; i++){
- if (typeof args[i]=='object'){
- if(args[i]) {
- for (var j=0; j<args[i].length; j++){
- returns.push(args[i][j]);
- }
+ if (!args[i]) {
+ continue;
+ }
+ if (args[i].constructor.name == 'Array') {
+ for (var j=0; j<args[i].length; j++){
+ returns.push(args[i][j]);
}
}
else {
@@ -987,7 +1033,7 @@ var Zotero = new function(){
}
- function reloadDataObjects() {
+ this.reloadDataObjects = function () {
Zotero.Tags.reloadAll();
Zotero.Collections.reloadAll();
Zotero.Creators.reloadAll();
@@ -2129,7 +2175,7 @@ Zotero.DragDrop = {
dragData.dataType = 'text/x-moz-url';
var urls = [];
for (var i=0; i<len; i++) {
- var url = dt.getData("application/x-moz-url", i).split("\n")[0];
+ var url = dt.getData("text/x-moz-url").split("\n")[0];
urls.push(url);
}
dragData.data = urls;
diff --git a/chrome/skin/default/zotero/group_add.png b/chrome/skin/default/zotero/group_add.png
Binary files differ.
diff --git a/chrome/skin/default/zotero/merge.css b/chrome/skin/default/zotero/merge.css
@@ -69,7 +69,7 @@ zoteromergegroup {
overflow-y: auto;
}
-zoteromergepane #delete-box {
+zoteromergepane #trash-box, zoteromergepane #delete-box {
min-width: 15em;
-moz-box-align: center;
-moz-box-pack: center;
diff --git a/chrome/skin/default/zotero/overlay.css b/chrome/skin/default/zotero/overlay.css
@@ -115,6 +115,12 @@ window[active="true"] #zotero-pane[fullscreenmode="true"][platform="mac"]
margin-top: -2px;
}
+#zotero-tb-group-add
+{
+ list-style-image: url('chrome://zotero/skin/group_add.png');
+}
+
+
#zotero-tb-tag-selector
{
list-style-image: url(chrome://zotero/skin/tag-selector.png);
@@ -283,6 +289,10 @@ window[active="true"] #zotero-pane[fullscreenmode="true"][platform="mac"]
list-style-image: url('chrome://zotero/skin/search-cancel-active.png');
}
+#zotero-view-tabs tab
+{
+}
+
#zotero-view-item > vbox
{
overflow: auto;
diff --git a/chrome/skin/default/zotero/treesource-groups.png b/chrome/skin/default/zotero/treesource-groups.png
Binary files differ.
diff --git a/components/zotero-protocol-handler.js b/components/zotero-protocol-handler.js
@@ -109,13 +109,14 @@ function ChromeExtensionHandler() {
break;
case 'search':
- var s = new Zotero.Search(ids);
- var ids = s.search();
+ var s = new Zotero.Search();
+ s.id = ids;
+ ids = s.search();
break;
case 'items':
case 'item':
- var ids = ids.split('-');
+ ids = ids.split('-');
break;
default:
diff --git a/components/zotero-service.js b/components/zotero-service.js
@@ -30,8 +30,13 @@ var xpcomFiles = [
'data/collections',
'data/creator',
'data/creators',
+ 'data/group',
+ 'data/groups',
'data/itemFields',
'data/notes',
+ 'data/libraries',
+ 'data/relation',
+ 'data/relations',
'data/tag',
'data/tags',
'db',
@@ -57,6 +62,7 @@ var xpcomFiles = [
'storage',
'timeline',
'translate',
+ 'uri',
'utilities',
'zeroconf'
];
diff --git a/install.rdf b/install.rdf
@@ -6,7 +6,7 @@
<em:id>zotero@chnm.gmu.edu</em:id>
<em:name>Zotero</em:name>
- <em:version>1.5b2.SVN</em:version>
+ <em:version>2.0b3.SVN</em:version>
<em:creator>Center for History and New Media<br/>George Mason University</em:creator>
<em:contributor>Dan Cohen</em:contributor>
<em:contributor>Sean Takats</em:contributor>
diff --git a/system.sql b/system.sql
@@ -1,4 +1,4 @@
--- 22
+-- 24
-- This file creates system tables that can be safely wiped and reinitialized
-- at any time, as long as existing ids are preserved.
@@ -275,6 +275,7 @@ INSERT INTO fields VALUES (114,'proceedingsTitle',NULL);
INSERT INTO fields VALUES (115,'bookTitle',NULL);
INSERT INTO fields VALUES (116,'shortTitle',NULL);
INSERT INTO fields VALUES (117,'docketNumber',NULL);
+INSERT INTO fields VALUES (118,'numPages',NULL);
INSERT INTO itemTypeFields VALUES (2, 110, NULL, 1);
INSERT INTO itemTypeFields VALUES (2, 90, NULL, 2);
@@ -286,7 +287,7 @@ INSERT INTO itemTypeFields VALUES (2, 6, NULL, 7);
INSERT INTO itemTypeFields VALUES (2, 7, NULL, 8);
INSERT INTO itemTypeFields VALUES (2, 8, NULL, 9);
INSERT INTO itemTypeFields VALUES (2, 14, NULL, 10);
-INSERT INTO itemTypeFields VALUES (2, 10, NULL, 11);
+INSERT INTO itemTypeFields VALUES (2, 118, NULL, 11);
INSERT INTO itemTypeFields VALUES (2, 87, NULL, 12);
INSERT INTO itemTypeFields VALUES (2, 11, NULL, 13);
INSERT INTO itemTypeFields VALUES (2, 116, NULL, 14);
@@ -408,7 +409,7 @@ INSERT INTO itemTypeFields VALUES (9, 90, NULL, 2);
INSERT INTO itemTypeFields VALUES (9, 66, NULL, 3);
INSERT INTO itemTypeFields VALUES (9, 7, NULL, 4);
INSERT INTO itemTypeFields VALUES (9, 14, NULL, 5);
-INSERT INTO itemTypeFields VALUES (9, 10, NULL, 6);
+INSERT INTO itemTypeFields VALUES (9, 118, NULL, 6);
INSERT INTO itemTypeFields VALUES (9, 87, NULL, 7);
INSERT INTO itemTypeFields VALUES (9, 116, NULL, 8);
INSERT INTO itemTypeFields VALUES (9, 1, NULL, 9);
@@ -1253,3 +1254,4 @@ INSERT INTO "syncObjectTypes" VALUES(2, 'creator');
INSERT INTO "syncObjectTypes" VALUES(3, 'item');
INSERT INTO "syncObjectTypes" VALUES(4, 'search');
INSERT INTO "syncObjectTypes" VALUES(5, 'tag');
+INSERT INTO "syncObjectTypes" VALUES(6, 'relations');
diff --git a/translators/zotero.org.js b/translators/zotero.org.js
@@ -3,7 +3,7 @@
"translatorType":4,
"label":"zotero.org",
"creator":"Dan Stillman",
- "target":"^https?://[^/]*zotero\\.org/[^/]+/[0-9]+/(items(/?[0-9]+?)?|items/collection/[0-9]+)(\\?.*)?$",
+ "target":"^https?://[^/]*zotero\\.net/(groups/)?[^/]+/[0-9]+/(items(/?[0-9]+?)?|items/collection/[0-9]+)(\\?.*)?$",
"minVersion":"1.0",
"maxVersion":"",
"priority":100,
@@ -16,7 +16,7 @@ function detectWeb(doc, url) {
var nsResolver = namespace ? function(prefix) {
if (prefix == 'x') return namespace; else return null;
} : null;
- var a = doc.evaluate('//div[@id="login-links"]/a[text()="My Library"]', doc, nsResolver, XPathResult.ANY_TYPE, null).iterateNext();
+ var a = doc.evaluate('//li[@class="topnav"]/a[text()="My Library"]', doc, nsResolver, XPathResult.ANY_TYPE, null).iterateNext();
// Skip current user's library
if (a && url.indexOf(a.href.match(/(^.+)\/items/)[1]) == 0) {
return false;
@@ -131,10 +131,20 @@ function xmlToItem(xmlItem) {
}
+
function doWeb(doc, url) {
- var userID = url.match(/^http:\/\/[^\/]*zotero\.org\/[^\/]+\/([0-9]+)/)[1];
- var apiPrefix = "https://api.zotero.org/users/" + userID + "/";
- var itemRe = /^http:\/\/[^\/]*zotero\.org\/[^\/]+\/[0-9]+\/items\/([0-9]+)/;
+ if (url.indexOf("/groups/") == -1) {
+ var userID = url.match(/^http:\/\/[^\/]*zotero\.net\/[^\/]+\/([0-9]+)/)[1];
+ var apiPrefix = "https://apidev.zotero.org/users/" + userID + "/";
+ var itemRe = /^http:\/\/[^\/]*zotero\.net\/[^\/]+\/[0-9]+\/items\/([0-9]+)/;
+ } else {
+ //var groupID = url.match(/^http:\/\/[^\/]*zotero\.net\/groups\/[^\/]+\/([0-9]+)/)[1]; // need slug url fix
+ var groupID = url.match(/^http:\/\/[^\/]*zotero\.net\/groups\/([0-9]+)/)[1];
+ var apiPrefix = "https://apidev.zotero.org/groups/" + groupID + "/";
+ //var itemRe = /^http:\/\/[^\/]*zotero\.net\/groups\/[^\/]+\/[0-9]+\/items\/([0-9]+)/;
+ var itemRe = /^http:\/\/[^\/]*zotero\.net\/groups\/[0-9]+\/items\/([0-9]+)/;
+ }
+
var nsAtom = new Namespace('http://www.w3.org/2005/Atom');
var nsZXfer = new Namespace('http://zotero.org/namespaces/transfer');
@@ -143,7 +153,7 @@ function doWeb(doc, url) {
var nsResolver = namespace ? function(prefix) {
if (prefix == 'x') return namespace; else return null;
} : null;
- var column = doc.evaluate('//table[@id="field-table"]//td[1][@class="title"]', doc, nsResolver, XPathResult.ANY_TYPE, null);
+ var column = doc.evaluate('//table[@id="field-table"]//td[1][@class="title"][not(contains(./a, "Unpublished Note"))]', doc, nsResolver, XPathResult.ANY_TYPE, null);
var elems = [], td;
while (td = column.iterateNext()) {
elems.push(td);
diff --git a/triggers.sql b/triggers.sql
@@ -1,4 +1,4 @@
--- 4
+-- 9
-- Triggers to validate date field
DROP TRIGGER IF EXISTS insert_date_field;
@@ -97,6 +97,47 @@ CREATE TRIGGER fku_collections_collectionID_collections_parentCollectionID
UPDATE collections SET parentCollectionID=NEW.collectionID WHERE parentCollectionID=OLD.collectionID;
END;
+-- Don't allow collection parents in different libraries
+DROP TRIGGER IF EXISTS fki_collections_parentCollectionID_libraryID;
+CREATE TRIGGER fki_collections_parentCollectionID_libraryID
+ BEFORE INSERT ON collections
+ FOR EACH ROW BEGIN
+ SELECT RAISE(ABORT, 'insert on table "collections" violates foreign key constraint "fki_collections_parentCollectionID_libraryID"')
+ WHERE NEW.parentCollectionID IS NOT NULL AND
+ (
+ (
+ NEW.libraryID IS NULL
+ AND
+ (SELECT libraryID FROM collections WHERE collectionID = NEW.parentCollectionID) IS NOT NULL
+ ) OR (
+ NEW.libraryID IS NOT NULL
+ AND
+ (SELECT libraryID FROM collections WHERE collectionID = NEW.parentCollectionID) IS NULL
+ ) OR
+ NEW.libraryID != (SELECT libraryID FROM collections WHERE collectionID = NEW.parentCollectionID)
+ );
+ END;
+
+DROP TRIGGER IF EXISTS fku_collections_parentCollectionID_libraryID;
+CREATE TRIGGER fku_collections_parentCollectionID_libraryID
+ BEFORE UPDATE ON collections
+ FOR EACH ROW BEGIN
+ SELECT RAISE(ABORT, 'update on table "collections" violates foreign key constraint "fku_collections_parentCollectionID_libraryID"')
+ WHERE NEW.parentCollectionID IS NOT NULL AND
+ (
+ (
+ NEW.libraryID IS NULL
+ AND
+ (SELECT libraryID FROM collections WHERE collectionID = NEW.parentCollectionID) IS NOT NULL
+ ) OR (
+ NEW.libraryID IS NOT NULL
+ AND
+ (SELECT libraryID FROM collections WHERE collectionID = NEW.parentCollectionID) IS NULL
+ ) OR
+ NEW.libraryID != (SELECT libraryID FROM collections WHERE collectionID = NEW.parentCollectionID)
+ );
+ END;
+
-- collectionItems/collectionID
DROP TRIGGER IF EXISTS fki_collectionItems_collectionID_collections_collectionID;
CREATE TRIGGER fki_collectionItems_collectionID_collections_collectionID
@@ -161,6 +202,76 @@ CREATE TRIGGER fku_items_itemID_collectionItems_itemID
UPDATE collectionItems SET collectionID=NEW.itemID WHERE collectionID=OLD.itemID;
END;
+-- collectionItems libraryID
+DROP TRIGGER IF EXISTS fki_collectionItems_libraryID;
+CREATE TRIGGER fki_collectionItems_libraryID
+ BEFORE INSERT ON collectionItems
+ FOR EACH ROW BEGIN
+ SELECT RAISE(ABORT, 'insert on table "collectionItems" violates foreign key constraint "fki_collectionItems_libraryID"')
+ WHERE (
+ (SELECT libraryID FROM collections WHERE collectionID = NEW.collectionID) IS NULL
+ AND
+ (SELECT libraryID FROM items WHERE itemID = NEW.itemID) IS NOT NULL
+ ) OR (
+ (SELECT libraryID FROM collections WHERE collectionID = NEW.collectionID) IS NOT NULL
+ AND
+ (SELECT libraryID FROM items WHERE itemID = NEW.itemID) IS NULL
+ ) OR
+ (SELECT libraryID FROM collections WHERE collectionID = NEW.collectionID) != (SELECT libraryID FROM items WHERE itemID = NEW.itemID);
+ END;
+
+DROP TRIGGER IF EXISTS fku_collectionItems_libraryID;
+CREATE TRIGGER fku_collectionItems_libraryID
+ BEFORE UPDATE ON collectionItems
+ FOR EACH ROW BEGIN
+ SELECT RAISE(ABORT, 'update on table "collectionItems" violates foreign key constraint "fku_collectionItems_libraryID"')
+ WHERE (
+ (SELECT libraryID FROM collections WHERE collectionID = NEW.collectionID) IS NULL
+ AND
+ (SELECT libraryID FROM items WHERE itemID = NEW.itemID) IS NOT NULL
+ ) OR (
+ (SELECT libraryID FROM collections WHERE collectionID = NEW.collectionID) IS NOT NULL
+ AND
+ (SELECT libraryID FROM items WHERE itemID = NEW.itemID) IS NULL
+ ) OR
+ (SELECT libraryID FROM collections WHERE collectionID = NEW.collectionID) != (SELECT libraryID FROM items WHERE itemID = NEW.itemID);
+ END;
+
+
+-- Don't allow child items to exist explicitly in collections
+DROP TRIGGER IF EXISTS fki_collectionItems_itemID_sourceItemID;
+CREATE TRIGGER fki_collectionItems_itemID_sourceItemID
+ BEFORE INSERT ON collectionItems
+ FOR EACH ROW BEGIN
+ SELECT RAISE(ABORT, 'insert on table "collectionItems" violates foreign key constraint "fki_collectionItems_itemID_sourceItemID"')
+ WHERE NEW.itemID IN (SELECT itemID FROM items WHERE itemID IN (SELECT itemID FROM itemAttachments WHERE sourceItemID IS NOT NULL) OR itemID IN (SELECT itemID FROM itemNotes WHERE sourceItemID IS NOT NULL));
+ END;
+
+DROP TRIGGER IF EXISTS fku_collectionItems_itemID_sourceItemID;
+CREATE TRIGGER fku_collectionItems_itemID_sourceItemID
+ BEFORE UPDATE OF itemID ON collectionItems
+ FOR EACH ROW BEGIN
+ SELECT RAISE(ABORT, 'update on table "collectionItems" violates foreign key constraint "fku_collectionItems_itemID_sourceItemID"')
+ WHERE NEW.itemID IN (SELECT itemID FROM items WHERE itemID IN (SELECT itemID FROM itemAttachments WHERE sourceItemID IS NOT NULL) OR itemID IN (SELECT itemID FROM itemNotes WHERE sourceItemID IS NOT NULL));
+ END;
+
+DROP TRIGGER IF EXISTS fku_itemAttachments_sourceItemID_collectionItems_itemID;
+CREATE TRIGGER fku_itemAttachments_sourceItemID_collectionItems_itemID
+ BEFORE UPDATE OF sourceItemID ON itemAttachments
+ FOR EACH ROW BEGIN
+ SELECT RAISE(ABORT, 'update on table "itemAttachments" violates foreign key constraint "fku_itemAttachments_sourceItemID_collectionItems_itemID"')
+ WHERE NEW.sourceItemID IS NOT NULL AND (SELECT COUNT(*) FROM collectionItems WHERE itemID = NEW.itemID) > 0;
+ END;
+
+DROP TRIGGER IF EXISTS fku_itemNotes_sourceItemID_collectionItems_itemID;
+CREATE TRIGGER fku_itemNotes_sourceItemID_collectionItems_itemID
+ BEFORE UPDATE OF sourceItemID ON itemNotes
+ FOR EACH ROW BEGIN
+ SELECT RAISE(ABORT, 'update on table "itemNotes" violates foreign key constraint "fku_itemNotes_sourceItemID_collectionItems_itemID"')
+ WHERE NEW.sourceItemID IS NOT NULL AND (SELECT COUNT(*) FROM collectionItems WHERE itemID = NEW.itemID) > 0;
+ END;
+
+
-- creators/creatorDataID
DROP TRIGGER IF EXISTS fki_creators_creatorDataID_creatorData_creatorDataID;
CREATE TRIGGER fki_creators_creatorDataID_creatorData_creatorDataID
@@ -292,6 +403,102 @@ CREATE TRIGGER fku_items_itemID_fulltextItemWords_itemID
UPDATE fulltextItemWords SET itemID=NEW.itemID WHERE itemID=OLD.itemID;
END;
+-- groups/libraryID
+DROP TRIGGER IF EXISTS fki_groups_libraryID_libraries_libraryID;
+CREATE TRIGGER fki_groups_libraryID_libraries_libraryID
+ BEFORE INSERT ON groups
+ FOR EACH ROW BEGIN
+ SELECT RAISE(ABORT, 'insert on table "groups" violates foreign key constraint "fki_groups_libraryID_libraries_libraryID"')
+ WHERE NEW.libraryID IS NOT NULL AND (SELECT COUNT(*) FROM libraries WHERE libraryID = NEW.libraryID) = 0;
+ END;
+
+DROP TRIGGER IF EXISTS fku_groups_libraryID_libraries_libraryID;
+CREATE TRIGGER fku_groups_libraryID_libraries_libraryID
+ BEFORE UPDATE OF libraryID ON groups
+ FOR EACH ROW BEGIN
+ SELECT RAISE(ABORT, 'update on table "groups" violates foreign key constraint "fku_groups_libraryID_libraries_libraryID"')
+ WHERE NEW.libraryID IS NOT NULL AND (SELECT COUNT(*) FROM libraries WHERE libraryID = NEW.libraryID) = 0;
+ END;
+
+DROP TRIGGER IF EXISTS fkd_groups_libraryID_libraries_libraryID;
+CREATE TRIGGER fkd_groups_libraryID_libraries_libraryID
+ BEFORE DELETE ON libraries
+ FOR EACH ROW BEGIN
+ SELECT RAISE(ABORT, 'delete on table "libraries" violates foreign key constraint "fkd_groups_libraryID_libraries_libraryID"')
+ WHERE (SELECT COUNT(*) FROM groups WHERE libraryID = OLD.libraryID) > 0;
+ END;
+
+DROP TRIGGER IF EXISTS fku_libraries_libraryID_groups_libraryID;
+CREATE TRIGGER fku_libraries_libraryID_groups_libraryID
+ AFTER UPDATE OF libraryID ON libraries
+ FOR EACH ROW BEGIN
+ UPDATE groups SET libraryID=NEW.libraryID WHERE libraryID=OLD.libraryID;
+ END;
+
+-- groupItems/createdByUserID
+DROP TRIGGER IF EXISTS fki_groupItems_createdByUserID_users_userID;
+CREATE TRIGGER fki_groupItems_createdByUserID_users_userID
+ BEFORE INSERT ON groupItems
+ FOR EACH ROW BEGIN
+ SELECT RAISE(ABORT, 'insert on table "groupItems" violates foreign key constraint "fki_groupItems_createdByUserID_users_userID"')
+ WHERE NEW.createdByUserID IS NOT NULL AND (SELECT COUNT(*) FROM users WHERE userID = NEW.createdByUserID) = 0;
+ END;
+
+DROP TRIGGER IF EXISTS fku_groupItems_createdByUserID_users_userID;
+CREATE TRIGGER fku_groupItems_createdByUserID_users_userID
+ BEFORE UPDATE OF createdByUserID ON groupItems
+ FOR EACH ROW BEGIN
+ SELECT RAISE(ABORT, 'update on table "groupItems" violates foreign key constraint "fku_groupItems_createdByUserID_users_userID"')
+ WHERE NEW.createdByUserID IS NOT NULL AND (SELECT COUNT(*) FROM users WHERE userID = NEW.createdByUserID) = 0;
+ END;
+
+DROP TRIGGER IF EXISTS fkd_groupItems_createdByUserID_users_userID;
+CREATE TRIGGER fkd_groupItems_createdByUserID_users_userID
+ BEFORE DELETE ON users
+ FOR EACH ROW BEGIN
+ SELECT RAISE(ABORT, 'delete on table "users" violates foreign key constraint "fkd_groupItems_createdByUserID_users_userID"')
+ WHERE (SELECT COUNT(*) FROM groupItems WHERE createdByUserID = OLD.userID) > 0;
+ END;
+
+DROP TRIGGER IF EXISTS fku_users_userID_groupItems_createdByUserID;
+CREATE TRIGGER fku_users_userID_groupItems_createdByUserID
+ AFTER UPDATE OF userID ON users
+ FOR EACH ROW BEGIN
+ UPDATE groupItems SET createdByUserID=NEW.userID WHERE createdByUserID=OLD.userID;
+ END;
+
+-- groupItems/lastModifiedByUserID
+DROP TRIGGER IF EXISTS fki_groupItems_lastModifiedByUserID_users_userID;
+CREATE TRIGGER fki_groupItems_lastModifiedByUserID_users_userID
+ BEFORE INSERT ON groupItems
+ FOR EACH ROW BEGIN
+ SELECT RAISE(ABORT, 'insert on table "groupItems" violates foreign key constraint "fki_groupItems_lastModifiedByUserID_users_userID"')
+ WHERE NEW.lastModifiedByUserID IS NOT NULL AND (SELECT COUNT(*) FROM users WHERE userID = NEW.lastModifiedByUserID) = 0;
+ END;
+
+DROP TRIGGER IF EXISTS fku_groupItems_lastModifiedByUserID_users_userID;
+CREATE TRIGGER fku_groupItems_lastModifiedByUserID_users_userID
+ BEFORE UPDATE OF lastModifiedByUserID ON groupItems
+ FOR EACH ROW BEGIN
+ SELECT RAISE(ABORT, 'update on table "groupItems" violates foreign key constraint "fku_groupItems_lastModifiedByUserID_users_userID"')
+ WHERE NEW.lastModifiedByUserID IS NOT NULL AND (SELECT COUNT(*) FROM users WHERE userID = NEW.lastModifiedByUserID) = 0;
+ END;
+
+DROP TRIGGER IF EXISTS fkd_groupItems_lastModifiedByUserID_users_userID;
+CREATE TRIGGER fkd_groupItems_lastModifiedByUserID_users_userID
+ BEFORE DELETE ON users
+ FOR EACH ROW BEGIN
+ SELECT RAISE(ABORT, 'delete on table "users" violates foreign key constraint "fkd_groupItems_lastModifiedByUserID_users_userID"')
+ WHERE (SELECT COUNT(*) FROM groupItems WHERE lastModifiedByUserID = OLD.userID) > 0;
+ END;
+
+DROP TRIGGER IF EXISTS fku_users_userID_groupItems_lastModifiedByUserID;
+CREATE TRIGGER fku_users_userID_groupItems_lastModifiedByUserID
+ AFTER UPDATE OF userID ON users
+ FOR EACH ROW BEGIN
+ UPDATE groupItems SET lastModifiedByUserID=NEW.userID WHERE lastModifiedByUserID=OLD.userID;
+ END;
+
-- highlights/itemID
DROP TRIGGER IF EXISTS fki_highlights_itemID_itemAttachments_itemID;
CREATE TRIGGER fki_highlights_itemID_itemAttachments_itemID
@@ -356,6 +563,49 @@ CREATE TRIGGER fku_items_itemID_itemAttachments_itemID
UPDATE itemAttachments SET itemID=NEW.itemID WHERE itemID=OLD.itemID;
END;
+
+-- itemAttachments libraryID
+DROP TRIGGER IF EXISTS fki_itemAttachments_libraryID;
+CREATE TRIGGER fki_itemAttachments_libraryID
+ BEFORE INSERT ON itemAttachments
+ FOR EACH ROW BEGIN
+ SELECT RAISE(ABORT, 'insert on table "itemAttachments" violates foreign key constraint "fki_itemAttachments_libraryID"')
+ WHERE
+ NEW.sourceItemID IS NOT NULL AND (
+ (
+ (SELECT libraryID FROM items WHERE itemID = NEW.itemID) IS NULL
+ AND
+ (SELECT libraryID FROM items WHERE itemID = NEW.sourceItemID) IS NOT NULL
+ ) OR (
+ (SELECT libraryID FROM items WHERE itemID = NEW.itemID) IS NOT NULL
+ AND
+ (SELECT libraryID FROM items WHERE itemID = NEW.sourceItemID) IS NULL
+ ) OR
+ (SELECT libraryID FROM items WHERE itemID = NEW.itemID) != (SELECT libraryID FROM items WHERE itemID = NEW.sourceItemID)
+ );
+ END;
+
+DROP TRIGGER IF EXISTS fku_itemAttachments_libraryID;
+CREATE TRIGGER fku_itemAttachments_libraryID
+ BEFORE UPDATE ON itemAttachments
+ FOR EACH ROW BEGIN
+ SELECT RAISE(ABORT, 'update on table "itemAttachments" violates foreign key constraint "fku_itemAttachments_libraryID"')
+ WHERE
+ NEW.sourceItemID IS NOT NULL AND (
+ (
+ (SELECT libraryID FROM items WHERE itemID = NEW.itemID) IS NULL
+ AND
+ (SELECT libraryID FROM items WHERE itemID = NEW.sourceItemID) IS NOT NULL
+ ) OR (
+ (SELECT libraryID FROM items WHERE itemID = NEW.itemID) IS NOT NULL
+ AND
+ (SELECT libraryID FROM items WHERE itemID = NEW.sourceItemID) IS NULL
+ ) OR
+ (SELECT libraryID FROM items WHERE itemID = NEW.itemID) != (SELECT libraryID FROM items WHERE itemID = NEW.sourceItemID)
+ );
+ END;
+
+
-- itemAttachments/sourceItemID
DROP TRIGGER IF EXISTS fki_itemAttachments_sourceItemID_items_itemID;
CREATE TRIGGER fki_itemAttachments_sourceItemID_items_itemID
@@ -485,6 +735,43 @@ CREATE TRIGGER fku_creatorTypes_creatorTypeID_itemCreators_creatorTypeID
WHERE (SELECT COUNT(*) FROM itemCreators WHERE creatorTypeID = OLD.creatorTypeID) > 0;
END;
+
+-- itemCreators libraryID
+DROP TRIGGER IF EXISTS fki_itemCreators_libraryID;
+CREATE TRIGGER fki_itemCreators_libraryID
+ BEFORE INSERT ON itemCreators
+ FOR EACH ROW BEGIN
+ SELECT RAISE(ABORT, 'insert on table "itemCreators" violates foreign key constraint "fki_itemCreators_libraryID"')
+ WHERE (
+ (SELECT libraryID FROM creators WHERE creatorID = NEW.creatorID) IS NULL
+ AND
+ (SELECT libraryID FROM items WHERE itemID = NEW.itemID) IS NOT NULL
+ ) OR (
+ (SELECT libraryID FROM creators WHERE creatorID = NEW.creatorID) IS NOT NULL
+ AND
+ (SELECT libraryID FROM items WHERE itemID = NEW.itemID) IS NULL
+ ) OR
+ (SELECT libraryID FROM creators WHERE creatorID = NEW.creatorID) != (SELECT libraryID FROM items WHERE itemID = NEW.itemID);
+ END;
+
+DROP TRIGGER IF EXISTS fku_itemCreators_libraryID;
+CREATE TRIGGER fku_itemCreators_libraryID
+ BEFORE UPDATE ON itemCreators
+ FOR EACH ROW BEGIN
+ SELECT RAISE(ABORT, 'update on table "itemCreators" violates foreign key constraint "fku_itemCreators_libraryID"')
+ WHERE (
+ (SELECT libraryID FROM creators WHERE creatorID = NEW.creatorID) IS NULL
+ AND
+ (SELECT libraryID FROM items WHERE itemID = NEW.itemID) IS NOT NULL
+ ) OR (
+ (SELECT libraryID FROM creators WHERE creatorID = NEW.creatorID) IS NOT NULL
+ AND
+ (SELECT libraryID FROM items WHERE itemID = NEW.itemID) IS NULL
+ ) OR
+ (SELECT libraryID FROM creators WHERE creatorID = NEW.creatorID) != (SELECT libraryID FROM items WHERE itemID = NEW.itemID);
+ END;
+
+
-- itemData/itemID
DROP TRIGGER IF EXISTS fki_itemData_itemID_items_itemID;
CREATE TRIGGER fki_itemData_itemID_items_itemID
@@ -615,6 +902,49 @@ CREATE TRIGGER fku_items_itemID_itemNotes_itemID
UPDATE itemNotes SET itemID=NEW.itemID WHERE itemID=OLD.itemID;
END;
+
+-- itemNotes libraryID
+DROP TRIGGER IF EXISTS fki_itemNotes_libraryID;
+CREATE TRIGGER fki_itemNotes_libraryID
+ BEFORE INSERT ON itemNotes
+ FOR EACH ROW BEGIN
+ SELECT RAISE(ABORT, 'insert on table "itemNotes" violates foreign key constraint "fki_itemNotes_libraryID"')
+ WHERE
+ NEW.sourceItemID IS NOT NULL AND (
+ (
+ (SELECT libraryID FROM items WHERE itemID = NEW.itemID) IS NULL
+ AND
+ (SELECT libraryID FROM items WHERE itemID = NEW.sourceItemID) IS NOT NULL
+ ) OR (
+ (SELECT libraryID FROM items WHERE itemID = NEW.itemID) IS NOT NULL
+ AND
+ (SELECT libraryID FROM items WHERE itemID = NEW.sourceItemID) IS NULL
+ ) OR
+ (SELECT libraryID FROM items WHERE itemID = NEW.itemID) != (SELECT libraryID FROM items WHERE itemID = NEW.sourceItemID)
+ );
+ END;
+
+DROP TRIGGER IF EXISTS fku_itemNotes_libraryID;
+CREATE TRIGGER fku_itemNotes_libraryID
+ BEFORE UPDATE ON itemNotes
+ FOR EACH ROW BEGIN
+ SELECT RAISE(ABORT, 'update on table "itemNotes" violates foreign key constraint "fku_itemNotes_libraryID"')
+ WHERE
+ NEW.sourceItemID IS NOT NULL AND (
+ (
+ (SELECT libraryID FROM items WHERE itemID = NEW.itemID) IS NULL
+ AND
+ (SELECT libraryID FROM items WHERE itemID = NEW.sourceItemID) IS NOT NULL
+ ) OR (
+ (SELECT libraryID FROM items WHERE itemID = NEW.itemID) IS NOT NULL
+ AND
+ (SELECT libraryID FROM items WHERE itemID = NEW.sourceItemID) IS NULL
+ ) OR
+ (SELECT libraryID FROM items WHERE itemID = NEW.itemID) != (SELECT libraryID FROM items WHERE itemID = NEW.sourceItemID)
+ );
+ END;
+
+
-- itemNotes/sourceItemID
DROP TRIGGER IF EXISTS fki_itemNotes_sourceItemID_items_itemID;
CREATE TRIGGER fki_itemNotes_sourceItemID_items_itemID
@@ -647,6 +977,38 @@ CREATE TRIGGER fku_items_itemID_itemNotes_sourceItemID
UPDATE itemNotes SET sourceItemID=NEW.itemID WHERE sourceItemID=OLD.itemID;
END;
+-- items/libraryID
+DROP TRIGGER IF EXISTS fki_items_libraryID_libraries_libraryID;
+CREATE TRIGGER fki_items_libraryID_libraries_libraryID
+ BEFORE INSERT ON items
+ FOR EACH ROW BEGIN
+ SELECT RAISE(ABORT, 'insert on table "items" violates foreign key constraint "fki_items_libraryID_libraries_libraryID"')
+ WHERE NEW.libraryID IS NOT NULL AND (SELECT COUNT(*) FROM libraries WHERE libraryID = NEW.libraryID) = 0;
+ END;
+
+DROP TRIGGER IF EXISTS fku_items_libraryID_libraries_libraryID;
+CREATE TRIGGER fku_items_libraryID_libraries_libraryID
+ BEFORE UPDATE OF libraryID ON items
+ FOR EACH ROW BEGIN
+ SELECT RAISE(ABORT, 'update on table "items" violates foreign key constraint "fku_items_libraryID_libraries_libraryID"')
+ WHERE NEW.libraryID IS NOT NULL AND (SELECT COUNT(*) FROM libraries WHERE libraryID = NEW.libraryID) = 0;
+ END;
+
+DROP TRIGGER IF EXISTS fkd_items_libraryID_libraries_libraryID;
+CREATE TRIGGER fkd_items_libraryID_libraries_libraryID
+ BEFORE DELETE ON libraries
+ FOR EACH ROW BEGIN
+ SELECT RAISE(ABORT, 'delete on table "libraries" violates foreign key constraint "fkd_items_libraryID_libraries_libraryID"')
+ WHERE (SELECT COUNT(*) FROM items WHERE libraryID = OLD.libraryID) > 0;
+ END;
+
+DROP TRIGGER IF EXISTS fku_libraries_libraryID_items_libraryID;
+CREATE TRIGGER fku_libraries_libraryID_items_libraryID
+ AFTER UPDATE OF libraryID ON libraries
+ FOR EACH ROW BEGIN
+ UPDATE items SET libraryID=NEW.libraryID WHERE libraryID=OLD.libraryID;
+ END;
+
-- itemSeeAlso/itemID
DROP TRIGGER IF EXISTS fki_itemSeeAlso_itemID_items_itemID;
CREATE TRIGGER fki_itemSeeAlso_itemID_items_itemID
@@ -711,6 +1073,43 @@ CREATE TRIGGER fku_items_itemID_itemSeeAlso_linkedItemID
UPDATE itemSeeAlso SET linkedItemID=NEW.itemID WHERE linkedItemID=OLD.itemID;
END;
+
+-- itemSeeAlso libraryID
+DROP TRIGGER IF EXISTS fki_itemSeeAlso_libraryID;
+CREATE TRIGGER fki_itemSeeAlso_libraryID
+ BEFORE INSERT ON itemSeeAlso
+ FOR EACH ROW BEGIN
+ SELECT RAISE(ABORT, 'insert on table "itemSeeAlso" violates foreign key constraint "fki_itemSeeAlso_libraryID"')
+ WHERE (
+ (SELECT libraryID FROM items WHERE itemID = NEW.itemID) IS NULL
+ AND
+ (SELECT libraryID FROM items WHERE itemID = NEW.linkedItemID) IS NOT NULL
+ ) OR (
+ (SELECT libraryID FROM items WHERE itemID = NEW.itemID) IS NOT NULL
+ AND
+ (SELECT libraryID FROM items WHERE itemID = NEW.linkedItemID) IS NULL
+ ) OR
+ (SELECT libraryID FROM items WHERE itemID = NEW.itemID) != (SELECT libraryID FROM items WHERE itemID = NEW.linkedItemID);
+ END;
+
+DROP TRIGGER IF EXISTS fku_itemSeeAlso_libraryID;
+CREATE TRIGGER fku_itemSeeAlso_libraryID
+ BEFORE UPDATE ON itemSeeAlso
+ FOR EACH ROW BEGIN
+ SELECT RAISE(ABORT, 'update on table "itemSeeAlso" violates foreign key constraint "fku_itemSeeAlso_libraryID"')
+ WHERE (
+ (SELECT libraryID FROM items WHERE itemID = NEW.itemID) IS NULL
+ AND
+ (SELECT libraryID FROM items WHERE itemID = NEW.linkedItemID) IS NOT NULL
+ ) OR (
+ (SELECT libraryID FROM items WHERE itemID = NEW.itemID) IS NOT NULL
+ AND
+ (SELECT libraryID FROM items WHERE itemID = NEW.linkedItemID) IS NULL
+ ) OR
+ (SELECT libraryID FROM items WHERE itemID = NEW.itemID) != (SELECT libraryID FROM items WHERE itemID = NEW.linkedItemID);
+ END;
+
+
-- itemTags/itemID
DROP TRIGGER IF EXISTS fki_itemTags_itemID_items_itemID;
CREATE TRIGGER fki_itemTags_itemID_items_itemID
@@ -743,6 +1142,43 @@ CREATE TRIGGER fkd_items_itemID_itemTags_itemID
UPDATE itemTags SET itemID=NEW.itemID WHERE itemID=OLD.itemID;
END;
+
+-- itemTags libraryID
+DROP TRIGGER IF EXISTS fki_itemTags_libraryID;
+CREATE TRIGGER fki_itemTags_libraryID
+ BEFORE INSERT ON itemTags
+ FOR EACH ROW BEGIN
+ SELECT RAISE(ABORT, 'insert on table "itemTags" violates foreign key constraint "fki_itemTags_libraryID"')
+ WHERE (
+ (SELECT libraryID FROM tags WHERE tagID = NEW.tagID) IS NULL
+ AND
+ (SELECT libraryID FROM items WHERE itemID = NEW.itemID) IS NOT NULL
+ ) OR (
+ (SELECT libraryID FROM tags WHERE tagID = NEW.tagID) IS NOT NULL
+ AND
+ (SELECT libraryID FROM items WHERE itemID = NEW.itemID) IS NULL
+ ) OR
+ (SELECT libraryID FROM tags WHERE tagID = NEW.tagID) != (SELECT libraryID FROM items WHERE itemID = NEW.itemID);
+ END;
+
+DROP TRIGGER IF EXISTS fku_itemTags_libraryID;
+CREATE TRIGGER fku_itemTags_libraryID
+ BEFORE UPDATE ON itemTags
+ FOR EACH ROW BEGIN
+ SELECT RAISE(ABORT, 'update on table "itemTags" violates foreign key constraint "fku_itemTags_libraryID"')
+ WHERE (
+ (SELECT libraryID FROM tags WHERE tagID = NEW.tagID) IS NULL
+ AND
+ (SELECT libraryID FROM items WHERE itemID = NEW.itemID) IS NOT NULL
+ ) OR (
+ (SELECT libraryID FROM tags WHERE tagID = NEW.tagID) IS NOT NULL
+ AND
+ (SELECT libraryID FROM items WHERE itemID = NEW.itemID) IS NULL
+ ) OR
+ (SELECT libraryID FROM tags WHERE tagID = NEW.tagID) != (SELECT libraryID FROM items WHERE itemID = NEW.itemID);
+ END;
+
+
-- itemTags/tagID
DROP TRIGGER IF EXISTS fki_itemTags_tagID_tags_tagID;
CREATE TRIGGER fki_itemTags_tagID_tags_tagID
@@ -775,6 +1211,7 @@ CREATE TRIGGER fku_tags_tagID_itemTags_tagID
UPDATE itemTags SET tagID=NEW.tagID WHERE tagID=OLD.tagID;
END;
+
-- savedSearchConditions/savedSearchID
DROP TRIGGER IF EXISTS fki_savedSearchConditions_savedSearchID_savedSearches_savedSearchID;
CREATE TRIGGER fki_savedSearchConditions_savedSearchID_savedSearches_savedSearchID
@@ -807,9 +1244,7 @@ CREATE TRIGGER fku_savedSearches_savedSearchID_savedSearchConditions_savedSearch
UPDATE savedSearchConditions SET savedSearchID=NEW.savedSearchID WHERE savedSearchID=OLD.savedSearchID;
END;
-
-- deletedItems/itemID
--- savedSearchConditions/savedSearchID
DROP TRIGGER IF EXISTS fki_deletedItems_itemID_items_itemID;
CREATE TRIGGER fki_deletedItems_itemID_items_itemID
BEFORE INSERT ON deletedItems
diff --git a/update.rdf b/update.rdf
@@ -7,7 +7,7 @@
<RDF:Seq>
<RDF:li>
<RDF:Description>
- <version>1.5b2.SVN</version>
+ <version>2.0b3.SVN</version>
<targetApplication>
<RDF:Description>
<id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</id>
diff --git a/userdata.sql b/userdata.sql
@@ -1,7 +1,7 @@
--- 50
+-- 53
--- This file creates tables containing user-specific data -- any changes made
--- here must be mirrored in transition steps in schema.js::_migrateSchema()
+-- This file creates tables containing user-specific data for new users --
+-- any changes made here must be mirrored in transition steps in schema.js::_migrateSchema()
CREATE TABLE version (
@@ -20,12 +20,17 @@ CREATE TABLE settings (
-- The foundational table; every item collected has a unique record here
CREATE TABLE items (
itemID INTEGER PRIMARY KEY,
- itemTypeID INT,
- dateAdded DATETIME DEFAULT CURRENT_TIMESTAMP,
- dateModified DATETIME DEFAULT CURRENT_TIMESTAMP,
- key TEXT NOT NULL UNIQUE
+ itemTypeID INT NOT NULL,
+ dateAdded TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ dateModified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ clientDateModified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ libraryID INT,
+ key TEXT NOT NULL,
+ UNIQUE (libraryID, key),
+ FOREIGN KEY (libraryID) REFERENCES libraries(libraryID)
);
+
CREATE TABLE itemDataValues (
valueID INTEGER PRIMARY KEY,
value UNIQUE
@@ -42,7 +47,7 @@ CREATE TABLE itemData (
FOREIGN KEY (valueID) REFERENCES itemDataValues(valueID)
);
--- Note data for note items
+-- Note data for note and attachment items
CREATE TABLE itemNotes (
itemID INTEGER PRIMARY KEY,
sourceItemID INT,
@@ -71,18 +76,19 @@ CREATE INDEX itemAttachments_sourceItemID ON itemAttachments(sourceItemID);
CREATE INDEX itemAttachments_mimeType ON itemAttachments(mimeType);
CREATE INDEX itemAttachments_syncState ON itemAttachments(syncState);
--- Individual entries for each tag
CREATE TABLE tags (
tagID INTEGER PRIMARY KEY,
- name TEXT COLLATE NOCASE,
+ name TEXT NOT NULL COLLATE NOCASE,
type INT NOT NULL,
- dateAdded DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
- dateModified DEFAULT CURRENT_TIMESTAMP NOT NULL,
- key TEXT NOT NULL UNIQUE,
- UNIQUE (name, type)
+ dateAdded TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ dateModified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ clientDateModified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ libraryID INT,
+ key TEXT NOT NULL,
+ UNIQUE (libraryID, name, type),
+ UNIQUE (libraryID, key)
);
--- Associates items with keywords
CREATE TABLE itemTags (
itemID INT,
tagID INT,
@@ -101,18 +107,20 @@ CREATE TABLE itemSeeAlso (
);
CREATE INDEX itemSeeAlso_linkedItemID ON itemSeeAlso(linkedItemID);
-
CREATE TABLE creators (
creatorID INTEGER PRIMARY KEY,
creatorDataID INT NOT NULL,
- dateAdded DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
- dateModified DEFAULT CURRENT_TIMESTAMP NOT NULL,
- key TEXT NOT NULL UNIQUE,
+ dateAdded TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ dateModified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ clientDateModified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ libraryID INT,
+ key TEXT NOT NULL,
+ UNIQUE (libraryID, key),
FOREIGN KEY (creatorDataID) REFERENCES creatorData(creatorDataID)
);
CREATE INDEX creators_creatorDataID ON creators(creatorDataID);
--- Each individual creator
+-- Unique creator data, which can be associated with more than one creator
CREATE TABLE creatorData (
creatorDataID INTEGER PRIMARY KEY,
firstName TEXT,
@@ -122,7 +130,6 @@ CREATE TABLE creatorData (
birthYear INT
);
--- Associates single or multiple creators to items
CREATE TABLE itemCreators (
itemID INT,
creatorID INT,
@@ -134,18 +141,19 @@ CREATE TABLE itemCreators (
FOREIGN KEY (creatorTypeID) REFERENCES creatorTypes(creatorTypeID)
);
--- Collections for holding items
CREATE TABLE collections (
collectionID INTEGER PRIMARY KEY,
- collectionName TEXT,
- parentCollectionID INT,
- dateAdded DEFAULT CURRENT_TIMESTAMP NOT NULL,
- dateModified DEFAULT CURRENT_TIMESTAMP NOT NULL,
- key TEXT NOT NULL UNIQUE,
+ collectionName TEXT NOT NULL,
+ parentCollectionID INT DEFAULT NULL,
+ dateAdded TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ dateModified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ clientDateModified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ libraryID INT,
+ key TEXT NOT NULL,
+ UNIQUE (libraryID, key),
FOREIGN KEY (parentCollectionID) REFERENCES collections(collectionID)
);
--- Associates items with the various collections they belong to
CREATE TABLE collectionItems (
collectionID INT,
itemID INT,
@@ -158,10 +166,13 @@ CREATE INDEX itemID ON collectionItems(itemID);
CREATE TABLE savedSearches (
savedSearchID INTEGER PRIMARY KEY,
- savedSearchName TEXT,
- dateAdded DEFAULT CURRENT_TIMESTAMP NOT NULL,
- dateModified DEFAULT CURRENT_TIMESTAMP NOT NULL,
- key TEXT NOT NULL UNIQUE
+ savedSearchName TEXT NOT NULL,
+ dateAdded TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ dateModified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ clientDateModified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ libraryID INT,
+ key TEXT NOT NULL,
+ UNIQUE (libraryID, key)
);
CREATE TABLE savedSearchConditions (
@@ -180,6 +191,44 @@ CREATE TABLE deletedItems (
dateDeleted DEFAULT CURRENT_TIMESTAMP NOT NULL
);
+CREATE TABLE relations (
+ libraryID INT NOT NULL,
+ subject TEXT NOT NULL,
+ predicate TEXT NOT NULL,
+ object TEXT NOT NULL,
+ clientDateModified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (subject, predicate, object)
+);
+CREATE INDEX relations_object ON relations(object);
+
+CREATE TABLE libraries (
+ libraryID INTEGER PRIMARY KEY,
+ libraryType TEXT NOT NULL
+);
+
+CREATE TABLE users (
+ userID INTEGER PRIMARY KEY,
+ username TEXT NOT NULL
+);
+
+CREATE TABLE groups (
+ groupID INTEGER PRIMARY KEY,
+ libraryID INT NOT NULL UNIQUE,
+ name TEXT NOT NULL,
+ description TEXT NOT NULL,
+ editable INT NOT NULL,
+ filesEditable INT NOT NULL,
+ FOREIGN KEY (libraryID) REFERENCES libraries(libraryID)
+);
+
+CREATE TABLE groupItems (
+ itemID INTEGER PRIMARY KEY,
+ createdByUserID INT NOT NULL,
+ lastModifiedByUserID INT NOT NULL,
+ FOREIGN KEY (createdByUserID) REFERENCES users(userID),
+ FOREIGN KEY (lastModifiedByUserID) REFERENCES users(userID)
+);
+
CREATE TABLE fulltextItems (
itemID INTEGER PRIMARY KEY,
version INT,
@@ -207,15 +256,19 @@ CREATE INDEX fulltextItemWords_itemID ON fulltextItemWords(itemID);
CREATE TABLE syncDeleteLog (
syncObjectTypeID INT NOT NULL,
- key TEXT NOT NULL UNIQUE,
+ libraryID INT,
+ key TEXT NOT NULL,
timestamp INT NOT NULL,
+ UNIQUE (libraryID, key),
FOREIGN KEY (syncObjectTypeID) REFERENCES syncObjectTypes(syncObjectTypeID)
);
CREATE INDEX syncDeleteLog_timestamp ON syncDeleteLog(timestamp);
CREATE TABLE storageDeleteLog (
- key TEXT PRIMARY KEY,
- timestamp INT NOT NULL
+ libraryID INT,
+ key TEXT NOT NULL,
+ timestamp INT NOT NULL,
+ PRIMARY KEY (libraryID, key)
);
CREATE INDEX storageDeleteLog_timestamp ON storageDeleteLog(timestamp);