diff --git a/chrome/content/zotero/itemPane.js b/chrome/content/zotero/itemPane.js index 4631317af..c20e0a48f 100644 --- a/chrome/content/zotero/itemPane.js +++ b/chrome/content/zotero/itemPane.js @@ -252,13 +252,17 @@ var ZoteroItemPane = new function() // Start tabindex at 1000 after creators var tabindex = editable ? (i>0 ? _tabIndexMinFields + i : 1) : 0; + _tabIndexMaxInfoFields = Math.max(_tabIndexMaxInfoFields, tabindex); + + if (fieldNames[i]=='date'){ + addDateRow(_itemBeingEdited.getField('date', true), tabindex); + continue; + } var valueElement = createValueElement( val, editable ? fieldNames[i] : null, tabindex ); - _tabIndexMaxInfoFields = Math.max(_tabIndexMaxInfoFields, tabindex); - var label = document.createElement("label"); label.setAttribute("value",Zotero.getString("itemFields."+fieldNames[i])+":"); label.setAttribute("onclick","this.nextSibling.blur();"); @@ -569,6 +573,49 @@ var ZoteroItemPane = new function() } } + + /** + * Add a date row with a label editor and a ymd indicator to show date parsing + */ + function addDateRow(value, tabindex) + { + var label = document.createElement("label"); + label.setAttribute("value", Zotero.getString("itemFields.date") + ':'); + label.setAttribute("fieldname",'date'); + label.setAttribute("onclick", "this.nextSibling.firstChild.blur()"); + + var hbox = document.createElement("hbox"); + var elem = createValueElement(Zotero.Date.multipartToStr(value), 'date', tabindex); + + // y-m-d status indicator + var datebox = document.createElement('hbox'); + datebox.className = 'zotero-date-field-status'; + var year = document.createElement('label'); + var month = document.createElement('label'); + var day = document.createElement('label'); + year.setAttribute('value', Zotero.getString('date.abbreviation.year')); + month.setAttribute('value', Zotero.getString('date.abbreviation.month')); + day.setAttribute('value', Zotero.getString('date.abbreviation.day')); + + // Display the date parts we have and hide the others + var sqldate = Zotero.Date.multipartToSQL(value); + year.setAttribute('hidden', !Zotero.Date.sqlHasYear(sqldate)); + month.setAttribute('hidden', !Zotero.Date.sqlHasMonth(sqldate)); + day.setAttribute('hidden', !Zotero.Date.sqlHasDay(sqldate)); + + datebox.appendChild(year); + datebox.appendChild(month); + datebox.appendChild(day); + + var hbox = document.createElement('hbox'); + hbox.setAttribute('flex', 1); + hbox.appendChild(elem); + hbox.appendChild(datebox); + + addDynamicRow(label, hbox); + } + + function switchCreatorMode(row, singleField, initial) { // Change if button position changes @@ -700,6 +747,7 @@ var ZoteroItemPane = new function() if(fieldName) { + valueElement.setAttribute('flex', 1); valueElement.setAttribute('fieldname',fieldName); valueElement.setAttribute('tabindex', tabindex); valueElement.setAttribute('onclick', 'ZoteroItemPane.showEditor(this)'); @@ -711,6 +759,12 @@ var ZoteroItemPane = new function() _tabIndexMaxTagsFields = Math.max(_tabIndexMaxTagsFields, tabindex); break; + // Display the SQL date as a tooltip for the date field + case 'date': + valueElement.setAttribute('tooltiptext', + Zotero.Date.multipartToSQL(_itemBeingEdited.getField('date', true))); + break; + // Convert dates from UTC case 'dateAdded': case 'dateModified': @@ -756,6 +810,7 @@ var ZoteroItemPane = new function() // Wrap to multiple lines valueElement.appendChild(document.createTextNode(valueText)); } + return valueElement; } @@ -1095,7 +1150,7 @@ var ZoteroItemPane = new function() if (fieldName=='accessDate' && value!='') { var localDate = Zotero.Date.sqlToDate(value); - var value = Zotero.Date.dateToSQL(localDate, true); + value = Zotero.Date.dateToSQL(localDate, true); } if(saveChanges) diff --git a/chrome/content/zotero/xpcom/data_access.js b/chrome/content/zotero/xpcom/data_access.js index 99a804e84..ec6164935 100644 --- a/chrome/content/zotero/xpcom/data_access.js +++ b/chrome/content/zotero/xpcom/data_access.js @@ -377,8 +377,11 @@ Zotero.Item.prototype.creatorExists = function(firstName, lastName, creatorTypeI * Retrieves (and loads from DB, if necessary) an itemData field value * * Field can be passed as fieldID or fieldName + * + * If _unformatted_ is true, skip any special processing of DB value + * (e.g. multipart date field) (default false) */ -Zotero.Item.prototype.getField = function(field){ +Zotero.Item.prototype.getField = function(field, unformatted){ //Zotero.debug('Requesting field ' + field + ' for item ' + this.getID(), 4); if (this.isPrimaryField(field)){ return this._data[field] ? this._data[field] : ''; @@ -390,7 +393,15 @@ Zotero.Item.prototype.getField = function(field){ var fieldID = Zotero.ItemFields.getID(field); - return this._itemData[fieldID] ? this._itemData[fieldID] : ''; + var value = this._itemData[fieldID] ? this._itemData[fieldID] : ''; + + if (!unformatted){ + if (fieldID==Zotero.ItemFields.getID('date')){ + value = Zotero.Date.multipartToStr(value); + } + } + + return value; } } @@ -443,6 +454,14 @@ Zotero.Item.prototype.setField = function(field, value, loadIn){ throw (field + ' is not a valid field for this type.'); } + // Save date field as multipart date + if (!loadIn){ + if (fieldID==Zotero.ItemFields.getID('date') && + !Zotero.Date.isMultipart(value)){ + value = Zotero.Date.strToMultipart(value); + } + } + // If existing value, make sure it's actually changing if ((!this._itemData[fieldID] && !value) || (this._itemData[fieldID] && this._itemData[fieldID]==value)){ @@ -632,14 +651,19 @@ Zotero.Item.prototype.save = function(){ // Take advantage of SQLite's manifest typing if (Zotero.ItemFields.isInteger(fieldID)){ updateStatement.bindInt32Parameter(0, - this.getField(fieldID)); + this.getField(fieldID, true)); } else { updateStatement.bindUTF8StringParameter(0, - this.getField(fieldID)); + this.getField(fieldID, true)); } updateStatement.bindInt32Parameter(2, fieldID); - updateStatement.execute(); + try { + updateStatement.execute(); + } + catch(e){ + throw(Zotero.DB.getLastErrorString()); + } } } @@ -663,14 +687,19 @@ Zotero.Item.prototype.save = function(){ else { if (Zotero.ItemFields.isInteger(fieldID)){ insertStatement.bindInt32Parameter(2, - this.getField(fieldID)); + this.getField(fieldID, true)); } else { insertStatement.bindUTF8StringParameter(2, - this.getField(fieldID)); + this.getField(fieldID, true)); } - insertStatement.execute(); + try { + insertStatement.execute(); + } + catch(e){ + throw(Zotero.DB.getLastErrorString()); + } } } } @@ -769,7 +798,7 @@ Zotero.Item.prototype.save = function(){ Zotero.DB.getStatement("INSERT INTO itemData VALUES (?,?,?)"); for (fieldID in this._changedItemData.items){ - if (!this.getField(fieldID)){ + if (!this.getField(fieldID, true)){ continue; } @@ -784,12 +813,17 @@ Zotero.Item.prototype.save = function(){ } else { if (Zotero.ItemFields.isInteger(fieldID)){ - statement.bindInt32Parameter(2, this.getField(fieldID)); + statement.bindInt32Parameter(2, this.getField(fieldID, true)); } else { - statement.bindUTF8StringParameter(2, this.getField(fieldID)); + statement.bindUTF8StringParameter(2, this.getField(fieldID, true)); + } + try { + statement.execute(); + } + catch(e){ + throw(Zotero.DB.getLastErrorString()); } - statement.execute(); } Zotero.History.add('itemData', 'itemID-fieldID', diff --git a/chrome/content/zotero/xpcom/schema.js b/chrome/content/zotero/xpcom/schema.js index e74177cb8..579ee9dae 100644 --- a/chrome/content/zotero/xpcom/schema.js +++ b/chrome/content/zotero/xpcom/schema.js @@ -315,6 +315,7 @@ Zotero.Schema = new function(){ Zotero.DB.query("PRAGMA auto_vacuum = 1"); Zotero.DB.query(_getSchemaSQL('userdata')); + _updateFailsafeSchema(); _updateDBVersion('userdata', _getSchemaSQLVersion('userdata')); Zotero.DB.query(_getSchemaSQL('system')); @@ -590,9 +591,20 @@ Zotero.Schema = new function(){ catch (e){} } } + + if (i==10){ + var dates = Zotero.DB.query("SELECT itemID, value FROM itemData WHERE fieldID=14"); + for each(var row in dates){ + if (!Zotero.Date.isMultipart(row.value)){ + Zotero.DB.query("UPDATE itemData SET value=? WHERE itemID=? AND fieldID=14", [Zotero.Date.strToMultipart(row.value), row.itemID]); + } + } + } } _updateSchema('userdata'); + _updateFailsafeSchema(); + Zotero.DB.commitTransaction(); } catch(e){ @@ -601,4 +613,38 @@ Zotero.Schema = new function(){ throw(e); } } + + + function _updateFailsafeSchema(){ + // This is super-annoying, but SQLite didn't have IF [NOT] EXISTS + // on trigger statements until 3.3.8, which didn't make it into + // Firefox 2.0, so we just throw the triggers at the DB on every + // userdata update and catch errors individually + // + // Add in DROP statements if these need to change + var itemDataTrigger = " FOR EACH ROW WHEN NEW.fieldID=14\n" + + " BEGIN\n" + + " SELECT CASE\n" + + " CAST(SUBSTR(NEW.value, 1, 4) AS INT) BETWEEN 0 AND 9999 AND\n" + + " SUBSTR(NEW.value, 5, 1) = '-' AND\n" + + " CAST(SUBSTR(NEW.value, 6, 2) AS INT) BETWEEN 0 AND 12 AND\n" + + " SUBSTR(NEW.value, 8, 1) = '-' AND\n" + + " CAST(SUBSTR(NEW.value, 9, 2) AS INT) BETWEEN 0 AND 31\n" + + " WHEN 0 THEN RAISE (ABORT, 'Date field must begin with SQL date') END;\n" + + " END;\n"; + + try { + var sql = "CREATE TRIGGER insert_date_field BEFORE INSERT ON itemData\n" + + itemDataTrigger; + Zotero.DB.query(sql); + } + catch (e){} + + try { + var sql = "CREATE TRIGGER update_date_field BEFORE UPDATE ON itemData\n" + + itemDataTrigger; + Zotero.DB.query(sql); + } + catch (e){} + } } diff --git a/chrome/content/zotero/xpcom/utilities.js b/chrome/content/zotero/xpcom/utilities.js index 3c2fc57e5..1bba3533a 100644 --- a/chrome/content/zotero/xpcom/utilities.js +++ b/chrome/content/zotero/xpcom/utilities.js @@ -158,7 +158,7 @@ Zotero.Utilities.prototype.inArray = Zotero.inArray; * pads a number or other string with a given string on the left */ Zotero.Utilities.prototype.lpad = function(string, pad, length) { - string = string + ''; + string = string ? string + '' : ''; while(string.length < length) { string = pad + string; } diff --git a/chrome/content/zotero/xpcom/zotero.js b/chrome/content/zotero/xpcom/zotero.js index a20746221..fcd2c4414 100644 --- a/chrome/content/zotero/xpcom/zotero.js +++ b/chrome/content/zotero/xpcom/zotero.js @@ -679,8 +679,18 @@ Zotero.Date = new function(){ this.dateToSQL = dateToSQL; this.strToDate = strToDate; this.formatDate = formatDate; + this.strToMultipart = strToMultipart; + this.isMultipart = isMultipart; + this.multipartToSQL = multipartToSQL; + this.multipartToStr = multipartToStr; + this.isSQLDate = isSQLDate; + this.sqlHasYear = sqlHasYear; + this.sqlHasMonth = sqlHasMonth; + this.sqlHasDay = sqlHasDay; this.getFileDateString = getFileDateString; this.getFileTimeString = getFileTimeString; + this.getLocaleDateOrder = getLocaleDateOrder; + /** * Convert an SQL date in the form '2006-06-13 11:03:05' into a JS Date object @@ -773,6 +783,7 @@ Zotero.Date = new function(){ var _yearRe = /^(.*)\b((?:circa |around |about |c\.? ?)?[0-9]{1,4}(?: ?B\.? ?C\.?(?: ?E\.?)?| ?C\.? ?E\.?| ?A\.? ?D\.?)|[0-9]{3,4})\b(.*)$/i; var _monthRe = null; var _dayRe = null; + function strToDate(string) { var date = new Object(); @@ -929,6 +940,93 @@ Zotero.Date = new function(){ return string; } + + function strToMultipart(str){ + if (!str){ + return ''; + } + + var utils = new Zotero.Utilities(); + + var parts = strToDate(str); + parts.month = typeof parts.month != undefined ? parts.month + 1 : ''; + + var multi = utils.lpad(parts.year, '0', 4) + '-' + + utils.lpad(parts.month, '0', 2) + '-' + + utils.lpad(parts.day, '0', 2) + + ' ' + + str; + + return multi; + } + + // Regexes for multipart and SQL dates + var _multipartRE = /^[0-9]{4}\-[0-9]{2}\-[0-9]{2} /; + var _sqldateRE = /^[0-9]{4}\-[0-9]{2}\-[0-9]{2}/; + + /** + * Tests if a string is a multipart date string + * e.g. '2006-11-03 November 3rd, 2006' + */ + function isMultipart(str){ + return _multipartRE.test(str); + } + + + /** + * Returns the SQL part of a multipart date string + * (e.g. '2006-11-03 November 3rd, 2006' returns '2006-11-03') + */ + function multipartToSQL(multi){ + if (!multi){ + return ''; + } + + if (!isMultipart(multi)){ + return '0000-00-00'; + } + + return multi.substr(0, 10); + } + + + /** + * Returns the user part of a multipart date string + * (e.g. '2006-11-03 November 3rd, 2006' returns 'November 3rd, 2006') + */ + function multipartToStr(multi){ + if (!multi){ + return ''; + } + + if (!isMultipart(multi)){ + return multi; + } + + return multi.substr(11); + } + + + function isSQLDate(str){ + return _sqldateRE.test(str); + } + + + function sqlHasYear(sqldate){ + return isSQLDate(sqldate) && sqldate.substr(0,4)!='0000'; + } + + + function sqlHasMonth(sqldate){ + return isSQLDate(sqldate) && sqldate.substr(5,2)!='00'; + } + + + function sqlHasDay(sqldate){ + return isSQLDate(sqldate) && sqldate.substr(8,2)!='00'; + } + + function getFileDateString(file){ var date = new Date(); date.setTime(file.lastModifiedTime); @@ -941,6 +1039,54 @@ Zotero.Date = new function(){ date.setTime(file.lastModifiedTime); return date.toLocaleTimeString(); } + + /** + * Figure out the date order from the output of toLocaleDateString() + * + * Note: Currently unused + * + * Returns a string with y, m, and d (e.g. 'ymd', 'mdy') + */ + function getLocaleDateOrder(){ + var date = new Date("October 5, 2006"); + var parts = date.toLocaleDateString().match(/([0-9]+)[^0-9]+([0-9]+)[^0-9]+([0-9]+)/); + alert(parts); + switch (parseInt(parts[1])){ + case 2006: + var order = 'y'; + break; + case 10: + var order = 'm'; + break; + case 5: + var order = 'd'; + break; + } + switch (parseInt(parts[2])){ + case 2006: + order += 'y'; + break; + case 10: + order += 'm'; + break; + case 5: + order += 'd'; + break; + } + switch (parseInt(parts[3])){ + case 2006: + order += 'y'; + break; + case 10: + order += 'm'; + break; + case 5: + order += 'd'; + break; + } + + return order; + } } Zotero.Browser = new function() { diff --git a/chrome/locale/en-US/zotero/zotero.properties b/chrome/locale/en-US/zotero/zotero.properties index 16273ffa7..1a686e052 100644 --- a/chrome/locale/en-US/zotero/zotero.properties +++ b/chrome/locale/en-US/zotero/zotero.properties @@ -234,6 +234,9 @@ exportOptions.exportNotes = Export Notes exportOptions.exportFileData = Export Files date.daySuffixes = st, nd, rd, th +date.abbreviation.year = y +date.abbreviation.month = m +date.abbreviation.day = d citation.multipleSources = Multiple Sources... citation.singleSource = Single Source... \ No newline at end of file diff --git a/chrome/skin/default/zotero/zotero.css b/chrome/skin/default/zotero/zotero.css index 709927a06..ccc1f4be5 100644 --- a/chrome/skin/default/zotero/zotero.css +++ b/chrome/skin/default/zotero/zotero.css @@ -28,6 +28,9 @@ toolbar[iconsize="small"] #zotero-toolbar-button:active list-style-image: url('chrome://zotero/skin/zotero-z-16px-active.png'); } + +/* Bindings */ + textbox[multiline="true"][type="timed"] { -moz-binding: url('chrome://zotero/content/bindings/timedtextarea.xml#timed-textarea'); @@ -48,6 +51,7 @@ tagsbox -moz-binding: url('chrome://zotero/content/bindings/tagsbox.xml#tags-box'); } + tagsbox row { -moz-box-align:center; @@ -169,6 +173,18 @@ zoterosearchtextbox .toolbarbutton-menu-dropmarker margin-left:6px; } +#zotero-editpane-dynamic-fields hbox.zotero-date-field-status +{ + margin-right:5px; +} + +#zotero-editpane-dynamic-fields hbox.zotero-date-field-status label +{ + font-weight: bold; + color: #666; + margin: 0 0 0 1px; +} + .zotero-clicky, .zotero-unclicky { -moz-border-radius: 6px; diff --git a/system.sql b/system.sql index 68c3fb7ca..0ee5f7aab 100644 --- a/system.sql +++ b/system.sql @@ -15,6 +15,8 @@ -- Describes various types of fields and their format restrictions, -- and indicates whether data should be stored as strings or integers + -- + -- unused DROP TABLE IF EXISTS fieldFormats; CREATE TABLE fieldFormats ( fieldFormatID INTEGER PRIMARY KEY, @@ -113,10 +115,11 @@ CREATE TABLE itemTypeCreatorTypes ( ); + -- unused INSERT INTO "fieldFormats" VALUES(1, '.*', 0); INSERT INTO "fieldFormats" VALUES(2, '[0-9]*', 1); INSERT INTO "fieldFormats" VALUES(3, '[0-9]{4}', 1); - + INSERT INTO itemTypes VALUES (1,'note',NULL,0); INSERT INTO itemTypes VALUES (2,'book',NULL,2); INSERT INTO itemTypes VALUES (3,'bookSection',2,2); @@ -570,7 +573,6 @@ INSERT INTO itemTypeFields VALUES (32, 1, NULL, 11); INSERT INTO itemTypeFields VALUES (32, 27, NULL, 12); INSERT INTO itemTypeFields VALUES (32, 22, NULL, 13); - INSERT INTO creatorTypes VALUES(1, "author"); INSERT INTO creatorTypes VALUES(2, "contributor"); INSERT INTO creatorTypes VALUES(3, "editor"); @@ -700,6 +702,7 @@ INSERT INTO itemTypeCreatorTypes VALUES(31,25,0); INSERT INTO itemTypeCreatorTypes VALUES(32,21,1); INSERT INTO itemTypeCreatorTypes VALUES(32,2,0); + INSERT INTO "fileTypes" VALUES(1, 'webpage'); INSERT INTO "fileTypes" VALUES(2, 'image'); INSERT INTO "fileTypes" VALUES(3, 'pdf'); diff --git a/userdata.sql b/userdata.sql index 3bc8d21ff..fbf722470 100644 --- a/userdata.sql +++ b/userdata.sql @@ -1,4 +1,4 @@ --- 9 +-- 10 -- This file creates tables containing user-specific data -- any changes -- to existing tables made here must be mirrored in transition steps in @@ -63,6 +63,8 @@ CREATE TABLE IF NOT EXISTS items ( ); -- Type-specific data for individual items +-- +-- Triggers specified in schema.js due to lack of trigger IF [NOT] EXISTS in Firefox 2.0 CREATE TABLE IF NOT EXISTS itemData ( itemID INT, fieldID INT,