itemFields.js (14207B)
1 /* 2 ***** BEGIN LICENSE BLOCK ***** 3 4 Copyright © 2009 Center for History and New Media 5 George Mason University, Fairfax, Virginia, USA 6 http://zotero.org 7 8 This file is part of Zotero. 9 10 Zotero is free software: you can redistribute it and/or modify 11 it under the terms of the GNU Affero General Public License as published by 12 the Free Software Foundation, either version 3 of the License, or 13 (at your option) any later version. 14 15 Zotero is distributed in the hope that it will be useful, 16 but WITHOUT ANY WARRANTY; without even the implied warranty of 17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 GNU Affero General Public License for more details. 19 20 You should have received a copy of the GNU Affero General Public License 21 along with Zotero. If not, see <http://www.gnu.org/licenses/>. 22 23 ***** END LICENSE BLOCK ***** 24 */ 25 26 27 Zotero.ItemFields = new function() { 28 // Private members 29 var _fields = {}; 30 var _fieldsFormats = []; 31 var _fieldsLoaded; 32 var _itemTypeFieldsLoaded; 33 var _fieldFormats = []; 34 var _itemTypeFields = []; 35 var _baseTypeFields = []; 36 var _baseMappedFields = []; 37 var _typeFieldIDsByBase = {}; 38 var _typeFieldNamesByBase = {}; 39 var _baseFieldIDsByTypeAndField = {}; 40 41 // Privileged methods 42 this.getName = getName; 43 this.getID = getID; 44 this.getLocalizedString = getLocalizedString; 45 this.isValidForType = isValidForType; 46 this.isInteger = isInteger; 47 this.getItemTypeFields = getItemTypeFields; 48 this.isBaseField = isBaseField; 49 this.isFieldOfBase = isFieldOfBase; 50 this.getFieldIDFromTypeAndBase = getFieldIDFromTypeAndBase; 51 this.getBaseIDFromTypeAndField = getBaseIDFromTypeAndField; 52 this.getTypeFieldsFromBase = getTypeFieldsFromBase; 53 54 55 /* 56 * Load all fields into an internal hash array 57 */ 58 this.init = Zotero.Promise.coroutine(function* () { 59 _fields = {}; 60 _fieldsFormats = []; 61 62 var result = yield Zotero.DB.queryAsync('SELECT * FROM fieldFormats'); 63 64 for (var i=0; i<result.length; i++) { 65 _fieldFormats[result[i]['fieldFormatID']] = { 66 regex: result[i]['regex'], 67 isInteger: result[i]['isInteger'] 68 }; 69 } 70 71 var fields = yield Zotero.DB.queryAsync('SELECT * FROM fieldsCombined'); 72 73 var fieldItemTypes = yield _getFieldItemTypes(); 74 75 var sql = "SELECT DISTINCT baseFieldID FROM baseFieldMappingsCombined"; 76 var baseFields = yield Zotero.DB.columnQueryAsync(sql); 77 78 for (let field of fields) { 79 _fields[field['fieldID']] = { 80 id: field['fieldID'], 81 name: field.fieldName, 82 label: field.label, 83 custom: !!field.custom, 84 isBaseField: (baseFields.indexOf(field['fieldID']) != -1), 85 formatID: field['fieldFormatID'], 86 itemTypes: fieldItemTypes[field['fieldID']] 87 }; 88 // Store by name as well as id 89 _fields[field['fieldName']] = _fields[field['fieldID']]; 90 } 91 92 _fieldsLoaded = true; 93 yield _loadBaseTypeFields(); 94 yield _loadItemTypeFields(); 95 }); 96 97 98 /* 99 * Return the fieldID for a passed fieldID or fieldName 100 */ 101 function getID(field) { 102 if (!_fieldsLoaded) { 103 throw new Zotero.Exception.UnloadedDataException("Item field data not yet loaded"); 104 } 105 106 if (typeof field == 'number') { 107 return _fields[field] ? field : false; 108 } 109 110 return _fields[field] ? _fields[field]['id'] : false; 111 } 112 113 114 /* 115 * Return the fieldName for a passed fieldID or fieldName 116 */ 117 function getName(field) { 118 if (!_fieldsLoaded) { 119 throw new Zotero.Exception.UnloadedDataException("Item field data not yet loaded"); 120 } 121 122 return _fields[field] ? _fields[field]['name'] : false; 123 } 124 125 126 function getLocalizedString(itemType, field) { 127 // unused currently 128 //var typeName = Zotero.ItemTypes.getName(itemType); 129 var fieldName = Zotero.ItemFields.getName(field); 130 131 // Fields in the items table are special cases 132 switch (field) { 133 case 'dateAdded': 134 case 'dateModified': 135 case 'itemType': 136 return Zotero.getString("itemFields." + field); 137 } 138 139 // TODO: different labels for different item types 140 141 _fieldCheck(field, 'getLocalizedString'); 142 143 if (_fields[field].label) { 144 return _fields[field].label; 145 } 146 else { 147 try { 148 var loc = Zotero.getString("itemFields." + fieldName); 149 } 150 // If localized string not found, try base field 151 catch (e) { 152 Zotero.debug("Localized string not found for field '" + fieldName + "' -- trying base field"); 153 var baseFieldID = this.getBaseIDFromTypeAndField(itemType, field); 154 fieldName = this.getName(baseFieldID); 155 var loc = Zotero.getString("itemFields." + fieldName); 156 } 157 return loc; 158 } 159 } 160 161 162 function isValidForType(fieldID, itemTypeID) { 163 fieldID = getID(fieldID); 164 if (!fieldID) return false; 165 166 if (!_fields[fieldID]['itemTypes']) { 167 return false; 168 } 169 170 return !!_fields[fieldID]['itemTypes'][itemTypeID]; 171 } 172 173 174 function isInteger(fieldID) { 175 _fieldCheck(fieldID, 'isInteger'); 176 177 var ffid = _fields[fieldID]['formatID']; 178 return _fieldFormats[ffid] ? _fieldFormats[ffid]['isInteger'] : false; 179 } 180 181 182 this.isCustom = function (fieldID) { 183 _fieldCheck(fieldID, 'isCustom'); 184 185 return _fields[fieldID].custom; 186 } 187 188 189 /* 190 * Returns an array of fieldIDs for a given item type 191 */ 192 function getItemTypeFields(itemTypeID) { 193 if (!itemTypeID) { 194 let e = new Error("Invalid item type id '" + itemTypeID + "'"); 195 e.name = "ZoteroUnknownTypeError"; 196 throw e; 197 } 198 199 if (!_itemTypeFieldsLoaded) { 200 throw new Zotero.Exception.UnloadedDataException("Item field data not yet loaded"); 201 } 202 203 if (!_itemTypeFields[itemTypeID]) { 204 throw new Error("Item type field data not found for itemTypeID " + itemTypeID); 205 } 206 207 return [..._itemTypeFields[itemTypeID]]; 208 } 209 210 211 function isBaseField(field) { 212 _fieldCheck(field, arguments.callee.name); 213 214 return _fields[field]['isBaseField']; 215 } 216 217 218 function isFieldOfBase(field, baseField) { 219 var fieldID = _fieldCheck(field, 'isFieldOfBase'); 220 221 var baseFieldID = this.getID(baseField); 222 if (!baseFieldID) { 223 throw new Error("Invalid field '" + baseField + '" for base field'); 224 } 225 226 if (fieldID == baseFieldID) { 227 return true; 228 } 229 230 var typeFields = this.getTypeFieldsFromBase(baseFieldID); 231 return typeFields.indexOf(fieldID) != -1; 232 } 233 234 235 this.getBaseMappedFields = function () { 236 return _baseMappedFields.concat(); 237 } 238 239 240 /* 241 * Returns the fieldID of a type-specific field for a given base field 242 * or false if none 243 * 244 * Examples: 245 * 246 * 'audioRecording' and 'publisher' returns label's fieldID 247 * 'book' and 'publisher' returns publisher's fieldID 248 * 'audioRecording' and 'number' returns false 249 * 250 * Accepts names or ids 251 */ 252 function getFieldIDFromTypeAndBase(itemType, baseField) { 253 var itemTypeID = Zotero.ItemTypes.getID(itemType); 254 if (!itemTypeID) { 255 throw new Error("Invalid item type '" + itemType + "'"); 256 } 257 258 var baseFieldID = this.getID(baseField); 259 if (!baseFieldID) { 260 throw new Error("Invalid field '" + baseField + '" for base field'); 261 } 262 263 // If field isn't a base field, return it if it's valid for the type 264 if (!this.isBaseField(baseFieldID)) { 265 return this.isValidForType(baseFieldID, itemTypeID) ? baseFieldID : false; 266 } 267 268 return _baseTypeFields[itemTypeID][baseFieldID]; 269 } 270 271 272 /* 273 * Returns the fieldID of the base field for a given type-specific field 274 * or false if none 275 * 276 * Examples: 277 * 278 * 'audioRecording' and 'label' returns publisher's fieldID 279 * 'book' and 'publisher' returns publisher's fieldID 280 * 'audioRecording' and 'runningTime' returns false 281 * 282 * Accepts names or ids 283 */ 284 function getBaseIDFromTypeAndField(itemType, typeField) { 285 var itemTypeID = Zotero.ItemTypes.getID(itemType); 286 var typeFieldID = this.getID(typeField); 287 288 if (!itemTypeID) { 289 throw new Error("Invalid item type '" + itemType + "'"); 290 } 291 292 _fieldCheck(typeField, 'getBaseIDFromTypeAndField'); 293 294 if (!this.isValidForType(typeFieldID, itemTypeID)) { 295 throw new Error("'" + typeField + "' is not a valid field for '" + itemType + "'"); 296 } 297 298 // If typeField is already a base field, just return that 299 if (this.isBaseField(typeFieldID)) { 300 return typeFieldID; 301 } 302 303 if (!_baseFieldIDsByTypeAndField[itemTypeID]) return false; 304 return _baseFieldIDsByTypeAndField[itemTypeID][typeFieldID] || false; 305 } 306 307 308 /* 309 * Returns an array of fieldIDs associated with a given base field 310 * 311 * e.g. 'publisher' returns fieldIDs for [university, studio, label, network] 312 */ 313 function getTypeFieldsFromBase(baseField, asNames) { 314 var baseFieldID = this.getID(baseField); 315 if (!baseFieldID) { 316 throw ("Invalid base field '" + baseField + '" in ItemFields.getTypeFieldsFromBase()'); 317 } 318 319 if (asNames) { 320 return _typeFieldNamesByBase[baseFieldID] ? 321 _typeFieldNamesByBase[baseFieldID] : false; 322 } 323 324 return _typeFieldIDsByBase[baseFieldID] ? 325 [..._typeFieldIDsByBase[baseFieldID]] : false; 326 } 327 328 329 this.isAutocompleteField = function (field) { 330 field = this.getName(field); 331 332 var autoCompleteFields = [ 333 'journalAbbreviation', 334 'series', 335 'seriesTitle', 336 'seriesText', 337 'libraryCatalog', 338 'callNumber', 339 'archive', 340 'archiveLocation', 341 'language', 342 'programmingLanguage', 343 'rights', 344 345 // TEMP - NSF 346 'programDirector', 347 'institution', 348 'discipline' 349 ]; 350 351 // Add the type-specific versions of these base fields 352 var baseACFields = ['publisher', 'publicationTitle', 'type', 'medium', 'place']; 353 autoCompleteFields = autoCompleteFields.concat(baseACFields); 354 355 for (var i=0; i<baseACFields.length; i++) { 356 var add = Zotero.ItemFields.getTypeFieldsFromBase(baseACFields[i], true) 357 autoCompleteFields = autoCompleteFields.concat(add); 358 } 359 360 return autoCompleteFields.indexOf(field) != -1; 361 } 362 363 364 /** 365 * A long field expands into a multiline textbox while editing but displays 366 * as a single line in non-editing mode; newlines are not allowed 367 */ 368 this.isLong = function (field) { 369 field = this.getName(field); 370 var fields = [ 371 'title', 372 'bookTitle' 373 ]; 374 return fields.indexOf(field) != -1; 375 } 376 377 378 /** 379 * A multiline field displays as a multiline text box in editing mode 380 * and non-editing mode; newlines are allowed 381 */ 382 this.isMultiline = function (field) { 383 field = this.getName(field); 384 var fields = [ 385 'abstractNote', 386 'extra', 387 388 // TEMP - NSF 389 'address' 390 ]; 391 return fields.indexOf(field) != -1; 392 } 393 394 395 /** 396 * Check whether a field is valid, throwing an exception if not 397 * (since it should never actually happen) 398 **/ 399 function _fieldCheck(field, func) { 400 var fieldID = Zotero.ItemFields.getID(field); 401 if (!fieldID) { 402 Zotero.debug((new Error).stack, 1); 403 throw new Error("Invalid field '" + field + (func ? "' in ItemFields." + func + "()" : "'")); 404 } 405 return fieldID; 406 } 407 408 409 /* 410 * Returns hash array of itemTypeIDs for which a given field is valid 411 */ 412 var _getFieldItemTypes = Zotero.Promise.coroutine(function* () { 413 var sql = 'SELECT fieldID, itemTypeID FROM itemTypeFieldsCombined'; 414 var results = yield Zotero.DB.queryAsync(sql); 415 416 if (!results) { 417 throw ('No fields in itemTypeFields!'); 418 } 419 var fields = []; 420 for (let i=0; i<results.length; i++) { 421 if (!fields[results[i].fieldID]) { 422 fields[results[i].fieldID] = []; 423 } 424 fields[results[i].fieldID][results[i].itemTypeID] = true; 425 } 426 return fields; 427 }); 428 429 430 /* 431 * Build a lookup table for base field mappings 432 */ 433 var _loadBaseTypeFields = Zotero.Promise.coroutine(function* () { 434 _typeFieldIDsByBase = {}; 435 _typeFieldNamesByBase = {}; 436 437 // Grab all fields, base field or not 438 var sql = "SELECT IT.itemTypeID, F.fieldID AS baseFieldID, BFM.fieldID " 439 + "FROM itemTypesCombined IT LEFT JOIN fieldsCombined F " 440 + "LEFT JOIN baseFieldMappingsCombined BFM" 441 + " ON (IT.itemTypeID=BFM.itemTypeID AND F.fieldID=BFM.baseFieldID)"; 442 var rows = yield Zotero.DB.queryAsync(sql); 443 444 var sql = "SELECT DISTINCT baseFieldID FROM baseFieldMappingsCombined"; 445 var baseFields = yield Zotero.DB.columnQueryAsync(sql); 446 447 var fields = []; 448 for (let row of rows) { 449 if (!fields[row.itemTypeID]) { 450 fields[row.itemTypeID] = []; 451 } 452 if (row.fieldID) { 453 fields[row.itemTypeID][row.baseFieldID] = row.fieldID; 454 } 455 // If a base field and already valid for the type, just use that 456 else if (isBaseField(row.baseFieldID) && 457 isValidForType(row.baseFieldID, row.itemTypeID)) { 458 fields[row.itemTypeID][row.baseFieldID] = row.baseFieldID; 459 } 460 // Set false for other fields so that we don't need to test for 461 // existence 462 else { 463 fields[row.itemTypeID][row.baseFieldID] = false; 464 } 465 } 466 _baseTypeFields = fields; 467 468 var sql = "SELECT itemTypeID, baseFieldID, fieldID, fieldName " 469 + "FROM baseFieldMappingsCombined JOIN fieldsCombined USING (fieldID)"; 470 var rows = yield Zotero.DB.queryAsync(sql); 471 for (let i = 0; i < rows.length; i++) { 472 let row = rows[i]; 473 // Type fields by base 474 if (!_typeFieldIDsByBase[row['baseFieldID']]) { 475 _typeFieldIDsByBase[row['baseFieldID']] = []; 476 _typeFieldNamesByBase[row['baseFieldID']] = []; 477 } 478 _typeFieldIDsByBase[row['baseFieldID']].push(row['fieldID']); 479 _typeFieldNamesByBase[row['baseFieldID']].push(row['fieldName']); 480 481 // Base fields by type and field 482 if (!_baseFieldIDsByTypeAndField[row.itemTypeID]) { 483 _baseFieldIDsByTypeAndField[row.itemTypeID] = {}; 484 } 485 _baseFieldIDsByTypeAndField[row.itemTypeID][row.fieldID] = row.baseFieldID; 486 487 } 488 489 // Get all fields mapped to base types 490 sql = "SELECT DISTINCT fieldID FROM baseFieldMappingsCombined"; 491 _baseMappedFields = yield Zotero.DB.columnQueryAsync(sql); 492 493 _baseTypeFieldsLoaded = true; 494 }); 495 496 497 var _loadItemTypeFields = Zotero.Promise.coroutine(function* () { 498 var sql = 'SELECT itemTypeID, fieldID FROM itemTypeFieldsCombined ORDER BY orderIndex'; 499 var rows = yield Zotero.DB.queryAsync(sql); 500 501 _itemTypeFields = {}; 502 _itemTypeFields[1] = []; // notes have no fields 503 504 for (let i=0; i<rows.length; i++) { 505 let row = rows[i]; 506 let itemTypeID = row.itemTypeID; 507 if (!_itemTypeFields[itemTypeID]) { 508 _itemTypeFields[itemTypeID] = []; 509 } 510 _itemTypeFields[itemTypeID].push(row.fieldID); 511 } 512 513 _itemTypeFieldsLoaded = true; 514 }); 515 }