From e73285ffc5736b290ab2d54d6c282b930e76df54 Mon Sep 17 00:00:00 2001 From: Dan Stillman Date: Fri, 3 Nov 2006 09:23:24 +0000 Subject: [PATCH] Improve date field handling - Item.setField() stores dates in a multipart format beginning with an SQL date followed by the user's entry, so "November 3, 2006" becomes "2006-11-03 November 3, 2006" -- date field entries are parsed with Zotero.Date.strToDate() if not already in multipart format - Item.getField() returns just the user part unless passed the new second parameter, _unformatted_, which returns the field directly from DB without processing (e.g. the full multipart string) - Added SQLite triggers on the itemData table to enforce multipart format even if the table is modified outside the API - Migration step to update existing dates - Indicator next to date field to show what we've parsed and a tooltip over the date field to show the SQL date -- though I'm not sure how well the abbreviation part will localize (i.e. can you abbreviate 'month' in Chinese?) One obvious problem is how to handle date ranges when sorting or searching, which may end up rendering this whole method fairly useless (though I guess the multipart format could begin with two SQL dates instead of just one, at the cost of some storage space...). Other changes: - Utilities.lpad() handling for undefined value parameter - new Zotero.Date methods: strToMultipart(), isMultipart(), multipartToSQL(), multipartToStr(), isSQLDate(), sqlHasYear(), sqlHasMonth, sqlHasDay getLocaleDateOrder() (the last one unused for now) - try/catch around manual itemData INSERT execute() statements in Item.save() --- chrome/content/zotero/itemPane.js | 61 +++++++- chrome/content/zotero/xpcom/data_access.js | 58 ++++++-- chrome/content/zotero/xpcom/schema.js | 46 ++++++ chrome/content/zotero/xpcom/utilities.js | 2 +- chrome/content/zotero/xpcom/zotero.js | 146 +++++++++++++++++++ chrome/locale/en-US/zotero/zotero.properties | 3 + chrome/skin/default/zotero/zotero.css | 16 ++ system.sql | 7 +- userdata.sql | 4 +- 9 files changed, 324 insertions(+), 19 deletions(-) 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,