From fefc75b8a770a8deaee4a0dbb90c4f8aea07ab12 Mon Sep 17 00:00:00 2001 From: Lebbeous Fogle-Weekley Date: Mon, 26 Mar 2012 13:53:58 -0400 Subject: [PATCH] early tests Signed-off-by: Lebbeous Fogle-Weekley --- Open-ILS/src/templates/conify/flattener_test.tt2 | 70 ++ Open-ILS/web/js/dojo/openils/DebuggingIFRS.js | 940 +++++++++++++++++++++ Open-ILS/web/js/dojo/openils/FlattenerStore.js | 280 ++++++ .../web/js/dojo/openils/widget/FlattenerGrid.js | 14 + 4 files changed, 1304 insertions(+) create mode 100644 Open-ILS/src/templates/conify/flattener_test.tt2 create mode 100644 Open-ILS/web/js/dojo/openils/DebuggingIFRS.js create mode 100644 Open-ILS/web/js/dojo/openils/FlattenerStore.js create mode 100644 Open-ILS/web/js/dojo/openils/widget/FlattenerGrid.js diff --git a/Open-ILS/src/templates/conify/flattener_test.tt2 b/Open-ILS/src/templates/conify/flattener_test.tt2 new file mode 100644 index 0000000000..55adcaad89 --- /dev/null +++ b/Open-ILS/src/templates/conify/flattener_test.tt2 @@ -0,0 +1,70 @@ +[% WRAPPER base.tt2 %] +[% ctx.page_title = 'Flattener Test' %] + +
+
+
Flattener Test
+
+ + +
+
+
+ blah, a dropdown or something here (optional) +
+ + + + + + + + + + +
Part NumberMinimum TemperatureMaximum TemperatureTypeThickness
+
+[% END %] diff --git a/Open-ILS/web/js/dojo/openils/DebuggingIFRS.js b/Open-ILS/web/js/dojo/openils/DebuggingIFRS.js new file mode 100644 index 0000000000..bf8de6b570 --- /dev/null +++ b/Open-ILS/web/js/dojo/openils/DebuggingIFRS.js @@ -0,0 +1,940 @@ +dojo.provide("openils.DebuggingIFRS"); + +dojo.require("dojo.data.util.filter"); +dojo.require("dojo.data.util.simpleFetch"); +dojo.require("dojo.date.stamp"); + +function lazy(o) { + if (typeof o == "object") { + if (o.part_num) + return "[object with part_num: " + o.part_num + "]"; + else + return "[object with keys: " + openils.Util.objectProperties(o).join(",") + "]"; + } else { + return o; + } +} + +dojo.declare("openils.DebuggingIFRS", null,{ + // summary: + // The ItemFileReadStore implements the dojo.data.api.Read API and reads + // data from JSON files that have contents in this format -- + // { items: [ + // { name:'Kermit', color:'green', age:12, friends:['Gonzo', {_reference:{name:'Fozzie Bear'}}]}, + // { name:'Fozzie Bear', wears:['hat', 'tie']}, + // { name:'Miss Piggy', pets:'Foo-Foo'} + // ]} + // Note that it can also contain an 'identifer' property that specified which attribute on the items + // in the array of items that acts as the unique identifier for that item. + // + constructor: function(/* Object */ keywordParameters){ + // summary: constructor + // keywordParameters: {url: String} + // keywordParameters: {data: jsonObject} + // keywordParameters: {typeMap: object) + // The structure of the typeMap object is as follows: + // { + // type0: function || object, + // type1: function || object, + // ... + // typeN: function || object + // } + // Where if it is a function, it is assumed to be an object constructor that takes the + // value of _value as the initialization parameters. If it is an object, then it is assumed + // to be an object of general form: + // { + // type: function, //constructor. + // deserialize: function(value) //The function that parses the value and constructs the object defined by type appropriately. + // } + + this._arrayOfAllItems = []; + this._arrayOfTopLevelItems = []; + this._loadFinished = false; + this._jsonFileUrl = keywordParameters.url; + this._jsonData = keywordParameters.data; + this._datatypeMap = keywordParameters.typeMap || {}; + if(!this._datatypeMap['Date']){ + //If no default mapping for dates, then set this as default. + //We use the dojo.date.stamp here because the ISO format is the 'dojo way' + //of generically representing dates. + this._datatypeMap['Date'] = { + type: Date, + deserialize: function(value){ + return dojo.date.stamp.fromISOString(value); + } + }; + } + this._features = {'dojo.data.api.Read':true, 'dojo.data.api.Identity':true}; + this._itemsByIdentity = null; + this._storeRefPropName = "_S"; // Default name for the store reference to attach to every item. + this._itemNumPropName = "_0"; // Default Item Id for isItem to attach to every item. + this._rootItemPropName = "_RI"; // Default Item Id for isItem to attach to every item. + this._reverseRefMap = "_RRM"; // Default attribute for constructing a reverse reference map for use with reference integrity + this._loadInProgress = false; //Got to track the initial load to prevent duelling loads of the dataset. + this._queuedFetches = []; + if(keywordParameters.urlPreventCache !== undefined){ + this.urlPreventCache = keywordParameters.urlPreventCache?true:false; + } + if(keywordParameters.clearOnClose){ + this.clearOnClose = true; + } + }, + + url: "", // use "" rather than undefined for the benefit of the parser (#3539) + + data: null, // define this so that the parser can populate it + + typeMap: null, //Define so parser can populate. + + //Parameter to allow users to specify if a close call should force a reload or not. + //By default, it retains the old behavior of not clearing if close is called. But + //if set true, the store will be reset to default state. Note that by doing this, + //all item handles will become invalid and a new fetch must be issued. + clearOnClose: false, + + //Parameter to allow specifying if preventCache should be passed to the xhrGet call or not when loading data from a url. + //Note this does not mean the store calls the server on each fetch, only that the data load has preventCache set as an option. + //Added for tracker: #6072 + urlPreventCache: false, + + _assertIsItem: function(/* item */ item){ + // summary: + // This function tests whether the item passed in is indeed an item in the store. + // item: + // The item to test for being contained by the store. + if(!this.isItem(item)){ + throw new Error("openils.DebuggingIFRS: Invalid item argument."); + } + }, + + _assertIsAttribute: function(/* attribute-name-string */ attribute){ + // summary: + // This function tests whether the item passed in is indeed a valid 'attribute' like type for the store. + // attribute: + // The attribute to test for being contained by the store. + if(typeof attribute !== "string"){ + throw new Error("openils.DebuggingIFRS: Invalid attribute argument."); + } + }, + + getValue: function( /* item */ item, + /* attribute-name-string */ attribute, + /* value? */ defaultValue){ + // summary: + // See dojo.data.api.Read.getValue() + console.log("getValue(" + lazy(item) + ", " + attribute + ", " + defaultValue + ")"); + var values = this.getValues(item, attribute); + return (values.length > 0)?values[0]:defaultValue; // mixed + }, + + getValues: function(/* item */ item, + /* attribute-name-string */ attribute){ + // summary: + // See dojo.data.api.Read.getValues() + console.log("getValues(" + item + ", " + attribute + ")"); + + this._assertIsItem(item); + this._assertIsAttribute(attribute); + return item[attribute] || []; // Array + }, + + getAttributes: function(/* item */ item){ + // summary: + // See dojo.data.api.Read.getAttributes() + console.log("getAttributes(" + item + ")"); + this._assertIsItem(item); + var attributes = []; + for(var key in item){ + // Save off only the real item attributes, not the special id marks for O(1) isItem. + if((key !== this._storeRefPropName) && (key !== this._itemNumPropName) && (key !== this._rootItemPropName) && (key !== this._reverseRefMap)){ + attributes.push(key); + } + } + return attributes; // Array + }, + + hasAttribute: function( /* item */ item, + /* attribute-name-string */ attribute) { + // summary: + // See dojo.data.api.Read.hasAttribute() + console.log("hasAttribute(" + item + ", " + attribute + ")"); + return this.getValues(item, attribute).length > 0; + }, + + containsValue: function(/* item */ item, + /* attribute-name-string */ attribute, + /* anything */ value){ + // summary: + // See dojo.data.api.Read.containsValue() + console.log("containsValue(" + item + ", " + attribute + ", " + value + ")"); + var regexp = undefined; + if(typeof value === "string"){ + regexp = dojo.data.util.filter.patternToRegExp(value, false); + } + return this._containsValue(item, attribute, value, regexp); //boolean. + }, + + _containsValue: function( /* item */ item, + /* attribute-name-string */ attribute, + /* anything */ value, + /* RegExp?*/ regexp){ + // summary: + // Internal function for looking at the values contained by the item. + // description: + // Internal function for looking at the values contained by the item. This + // function allows for denoting if the comparison should be case sensitive for + // strings or not (for handling filtering cases where string case should not matter) + // + // item: + // The data item to examine for attribute values. + // attribute: + // The attribute to inspect. + // value: + // The value to match. + // regexp: + // Optional regular expression generated off value if value was of string type to handle wildcarding. + // If present and attribute values are string, then it can be used for comparison instead of 'value' + return dojo.some(this.getValues(item, attribute), function(possibleValue){ + if(possibleValue !== null && !dojo.isObject(possibleValue) && regexp){ + if(possibleValue.toString().match(regexp)){ + return true; // Boolean + } + }else if(value === possibleValue){ + return true; // Boolean + } + }); + }, + + isItem: function(/* anything */ something){ + // summary: + // See dojo.data.api.Read.isItem() + console.log("isItem(" + lazy(something) + ")"); + if(something && something[this._storeRefPropName] === this){ + if(this._arrayOfAllItems[something[this._itemNumPropName]] === something){ + return true; + } + } + return false; // Boolean + }, + + isItemLoaded: function(/* anything */ something){ + console.log("isItemLoaded(" + something + ")"); + // summary: + // See dojo.data.api.Read.isItemLoaded() + return this.isItem(something); //boolean + }, + + loadItem: function(/* object */ keywordArgs){ + console.log("loadItem(" + dojo.toJson(keywordArgs) + ")"); + // summary: + // See dojo.data.api.Read.loadItem() + this._assertIsItem(keywordArgs.item); + }, + + getFeatures: function(){ + console.log("getFeatures()"); + // summary: + // See dojo.data.api.Read.getFeatures() + return this._features; //Object + }, + + getLabel: function(/* item */ item){ + // summary: + // See dojo.data.api.Read.getLabel() + var rv; + if(this._labelAttr && this.isItem(item)){ + rv = this.getValue(item,this._labelAttr); //String + } + console.log("getLabel(" + something + ") -> " + rv); + + return rv; //undefined + }, + + getLabelAttributes: function(/* item */ item){ + console.log("getLabelAttributes(" + item + ")"); + // summary: + // See dojo.data.api.Read.getLabelAttributes() + if(this._labelAttr){ + return [this._labelAttr]; //array + } + return null; //null + }, + + _fetchItems: function( /* Object */ keywordArgs, + /* Function */ findCallback, + /* Function */ errorCallback){ + // summary: + // See dojo.data.util.simpleFetch.fetch() + var self = this; + var filter = function(requestArgs, arrayOfItems){ + var items = []; + var i, key; + if(requestArgs.query){ + var value; + var ignoreCase = requestArgs.queryOptions ? requestArgs.queryOptions.ignoreCase : false; + + //See if there are any string values that can be regexp parsed first to avoid multiple regexp gens on the + //same value for each item examined. Much more efficient. + var regexpList = {}; + for(key in requestArgs.query){ + value = requestArgs.query[key]; + if(typeof value === "string"){ + regexpList[key] = dojo.data.util.filter.patternToRegExp(value, ignoreCase); + } + } + + for(i = 0; i < arrayOfItems.length; ++i){ + var match = true; + var candidateItem = arrayOfItems[i]; + if(candidateItem === null){ + match = false; + }else{ + for(key in requestArgs.query) { + value = requestArgs.query[key]; + if (!self._containsValue(candidateItem, key, value, regexpList[key])){ + match = false; + } + } + } + if(match){ + items.push(candidateItem); + } + } + findCallback(items, requestArgs); + }else{ + // We want a copy to pass back in case the parent wishes to sort the array. + // We shouldn't allow resort of the internal list, so that multiple callers + // can get lists and sort without affecting each other. We also need to + // filter out any null values that have been left as a result of deleteItem() + // calls in ItemFileWriteStore. + for(i = 0; i < arrayOfItems.length; ++i){ + var item = arrayOfItems[i]; + if(item !== null){ + items.push(item); + } + } + findCallback(items, requestArgs); + } + }; + + if(this._loadFinished){ + filter(keywordArgs, this._getItemsArray(keywordArgs.queryOptions)); + }else{ + + if(this._jsonFileUrl){ + //If fetches come in before the loading has finished, but while + //a load is in progress, we have to defer the fetching to be + //invoked in the callback. + if(this._loadInProgress){ + this._queuedFetches.push({args: keywordArgs, filter: filter}); + }else{ + this._loadInProgress = true; + var getArgs = { + url: self._jsonFileUrl, + handleAs: "json-comment-optional", + preventCache: this.urlPreventCache + }; + var getHandler = dojo.xhrGet(getArgs); + getHandler.addCallback(function(data){ + try{ + self._getItemsFromLoadedData(data); + self._loadFinished = true; + self._loadInProgress = false; + + filter(keywordArgs, self._getItemsArray(keywordArgs.queryOptions)); + self._handleQueuedFetches(); + }catch(e){ + self._loadFinished = true; + self._loadInProgress = false; + errorCallback(e, keywordArgs); + } + }); + getHandler.addErrback(function(error){ + self._loadInProgress = false; + errorCallback(error, keywordArgs); + }); + + //Wire up the cancel to abort of the request + //This call cancel on the deferred if it hasn't been called + //yet and then will chain to the simple abort of the + //simpleFetch keywordArgs + var oldAbort = null; + if(keywordArgs.abort){ + oldAbort = keywordArgs.abort; + } + keywordArgs.abort = function(){ + var df = getHandler; + if (df && df.fired === -1){ + df.cancel(); + df = null; + } + if(oldAbort){ + oldAbort.call(keywordArgs); + } + }; + } + }else if(this._jsonData){ + try{ + this._loadFinished = true; + this._getItemsFromLoadedData(this._jsonData); + this._jsonData = null; + filter(keywordArgs, this._getItemsArray(keywordArgs.queryOptions)); + }catch(e){ + errorCallback(e, keywordArgs); + } + }else{ + errorCallback(new Error("openils.DebuggingIFRS: No JSON source data was provided as either URL or a nested Javascript object."), keywordArgs); + } + } + }, + + _handleQueuedFetches: function(){ + // summary: + // Internal function to execute delayed request in the store. + //Execute any deferred fetches now. + if (this._queuedFetches.length > 0) { + for(var i = 0; i < this._queuedFetches.length; i++){ + var fData = this._queuedFetches[i]; + var delayedQuery = fData.args; + var delayedFilter = fData.filter; + if(delayedFilter){ + delayedFilter(delayedQuery, this._getItemsArray(delayedQuery.queryOptions)); + }else{ + this.fetchItemByIdentity(delayedQuery); + } + } + this._queuedFetches = []; + } + }, + + _getItemsArray: function(/*object?*/queryOptions){ + // summary: + // Internal function to determine which list of items to search over. + // queryOptions: The query options parameter, if any. + if(queryOptions && queryOptions.deep) { + return this._arrayOfAllItems; + } + return this._arrayOfTopLevelItems; + }, + + close: function(/*dojo.data.api.Request || keywordArgs || null */ request){ + console.log("close(" + dojo.toJson(request) + ")"); + // summary: + // See dojo.data.api.Read.close() + if(this.clearOnClose && (this._jsonFileUrl !== "")){ + //Reset all internalsback to default state. This will force a reload + //on next fetch, but only if the data came from a url. Passed in data + //means it should not clear the data. + this._arrayOfAllItems = []; + this._arrayOfTopLevelItems = []; + this._loadFinished = false; + this._itemsByIdentity = null; + this._loadInProgress = false; + this._queuedFetches = []; + } + }, + + _getItemsFromLoadedData: function(/* Object */ dataObject){ + // summary: + // Function to parse the loaded data into item format and build the internal items array. + // description: + // Function to parse the loaded data into item format and build the internal items array. + // + // dataObject: + // The JS data object containing the raw data to convery into item format. + // + // returns: array + // Array of items in store item format. + + // First, we define a couple little utility functions... + var addingArrays = false; + + function valueIsAnItem(/* anything */ aValue){ + // summary: + // Given any sort of value that could be in the raw json data, + // return true if we should interpret the value as being an + // item itself, rather than a literal value or a reference. + // example: + // | false == valueIsAnItem("Kermit"); + // | false == valueIsAnItem(42); + // | false == valueIsAnItem(new Date()); + // | false == valueIsAnItem({_type:'Date', _value:'May 14, 1802'}); + // | false == valueIsAnItem({_reference:'Kermit'}); + // | true == valueIsAnItem({name:'Kermit', color:'green'}); + // | true == valueIsAnItem({iggy:'pop'}); + // | true == valueIsAnItem({foo:42}); + var isItem = ( + (aValue !== null) && + (typeof aValue === "object") && + (!dojo.isArray(aValue) || addingArrays) && + (!dojo.isFunction(aValue)) && + (aValue.constructor == Object || dojo.isArray(aValue)) && + (typeof aValue._reference === "undefined") && + (typeof aValue._type === "undefined") && + (typeof aValue._value === "undefined") + ); + return isItem; + } + + var self = this; + function addItemAndSubItemsToArrayOfAllItems(/* Item */ anItem){ + self._arrayOfAllItems.push(anItem); + for(var attribute in anItem){ + var valueForAttribute = anItem[attribute]; + if(valueForAttribute){ + if(dojo.isArray(valueForAttribute)){ + var valueArray = valueForAttribute; + for(var k = 0; k < valueArray.length; ++k){ + var singleValue = valueArray[k]; + if(valueIsAnItem(singleValue)){ + addItemAndSubItemsToArrayOfAllItems(singleValue); + } + } + }else{ + if(valueIsAnItem(valueForAttribute)){ + addItemAndSubItemsToArrayOfAllItems(valueForAttribute); + } + } + } + } + } + + this._labelAttr = dataObject.label; + + // We need to do some transformations to convert the data structure + // that we read from the file into a format that will be convenient + // to work with in memory. + + // Step 1: Walk through the object hierarchy and build a list of all items + var i; + var item; + this._arrayOfAllItems = []; + this._arrayOfTopLevelItems = dataObject.items; + + for(i = 0; i < this._arrayOfTopLevelItems.length; ++i){ + item = this._arrayOfTopLevelItems[i]; + if(dojo.isArray(item)){ + addingArrays = true; + } + addItemAndSubItemsToArrayOfAllItems(item); + item[this._rootItemPropName]=true; + } + + // Step 2: Walk through all the attribute values of all the items, + // and replace single values with arrays. For example, we change this: + // { name:'Miss Piggy', pets:'Foo-Foo'} + // into this: + // { name:['Miss Piggy'], pets:['Foo-Foo']} + // + // We also store the attribute names so we can validate our store + // reference and item id special properties for the O(1) isItem + var allAttributeNames = {}; + var key; + + for(i = 0; i < this._arrayOfAllItems.length; ++i){ + item = this._arrayOfAllItems[i]; + for(key in item){ + if (key !== this._rootItemPropName) + { + var value = item[key]; + if(value !== null){ + if(!dojo.isArray(value)){ + item[key] = [value]; + } + }else{ + item[key] = [null]; + } + } + allAttributeNames[key]=key; + } + } + + // Step 3: Build unique property names to use for the _storeRefPropName and _itemNumPropName + // This should go really fast, it will generally never even run the loop. + while(allAttributeNames[this._storeRefPropName]){ + this._storeRefPropName += "_"; + } + while(allAttributeNames[this._itemNumPropName]){ + this._itemNumPropName += "_"; + } + while(allAttributeNames[this._reverseRefMap]){ + this._reverseRefMap += "_"; + } + + // Step 4: Some data files specify an optional 'identifier', which is + // the name of an attribute that holds the identity of each item. + // If this data file specified an identifier attribute, then build a + // hash table of items keyed by the identity of the items. + var arrayOfValues; + + var identifier = dataObject.identifier; + if(identifier){ + this._itemsByIdentity = {}; + this._features['dojo.data.api.Identity'] = identifier; + for(i = 0; i < this._arrayOfAllItems.length; ++i){ + item = this._arrayOfAllItems[i]; + arrayOfValues = item[identifier]; + var identity = arrayOfValues[0]; + if(!this._itemsByIdentity[identity]){ + this._itemsByIdentity[identity] = item; + }else{ + if(this._jsonFileUrl){ + throw new Error("openils.DebuggingIFRS: The json data as specified by: [" + this._jsonFileUrl + "] is malformed. Items within the list have identifier: [" + identifier + "]. Value collided: [" + identity + "]"); + }else if(this._jsonData){ + throw new Error("openils.DebuggingIFRS: The json data provided by the creation arguments is malformed. Items within the list have identifier: [" + identifier + "]. Value collided: [" + identity + "]"); + } + } + } + }else{ + this._features['dojo.data.api.Identity'] = Number; + } + + // Step 5: Walk through all the items, and set each item's properties + // for _storeRefPropName and _itemNumPropName, so that store.isItem() will return true. + for(i = 0; i < this._arrayOfAllItems.length; ++i){ + item = this._arrayOfAllItems[i]; + item[this._storeRefPropName] = this; + item[this._itemNumPropName] = i; + } + + // Step 6: We walk through all the attribute values of all the items, + // looking for type/value literals and item-references. + // + // We replace item-references with pointers to items. For example, we change: + // { name:['Kermit'], friends:[{_reference:{name:'Miss Piggy'}}] } + // into this: + // { name:['Kermit'], friends:[miss_piggy] } + // (where miss_piggy is the object representing the 'Miss Piggy' item). + // + // We replace type/value pairs with typed-literals. For example, we change: + // { name:['Nelson Mandela'], born:[{_type:'Date', _value:'July 18, 1918'}] } + // into this: + // { name:['Kermit'], born:(new Date('July 18, 1918')) } + // + // We also generate the associate map for all items for the O(1) isItem function. + for(i = 0; i < this._arrayOfAllItems.length; ++i){ + item = this._arrayOfAllItems[i]; // example: { name:['Kermit'], friends:[{_reference:{name:'Miss Piggy'}}] } + for(key in item){ + arrayOfValues = item[key]; // example: [{_reference:{name:'Miss Piggy'}}] + for(var j = 0; j < arrayOfValues.length; ++j) { + value = arrayOfValues[j]; // example: {_reference:{name:'Miss Piggy'}} + if(value !== null && typeof value == "object"){ + if(value._type && value._value){ + var type = value._type; // examples: 'Date', 'Color', or 'ComplexNumber' + var mappingObj = this._datatypeMap[type]; // examples: Date, dojo.Color, foo.math.ComplexNumber, {type: dojo.Color, deserialize(value){ return new dojo.Color(value)}} + if(!mappingObj){ + throw new Error("openils.DebuggingIFRS: in the typeMap constructor arg, no object class was specified for the datatype '" + type + "'"); + }else if(dojo.isFunction(mappingObj)){ + arrayOfValues[j] = new mappingObj(value._value); + }else if(dojo.isFunction(mappingObj.deserialize)){ + arrayOfValues[j] = mappingObj.deserialize(value._value); + }else{ + throw new Error("openils.DebuggingIFRS: Value provided in typeMap was neither a constructor, nor a an object with a deserialize function"); + } + } + if(value._reference){ + var referenceDescription = value._reference; // example: {name:'Miss Piggy'} + if(!dojo.isObject(referenceDescription)){ + // example: 'Miss Piggy' + // from an item like: { name:['Kermit'], friends:[{_reference:'Miss Piggy'}]} + arrayOfValues[j] = this._itemsByIdentity[referenceDescription]; + }else{ + // example: {name:'Miss Piggy'} + // from an item like: { name:['Kermit'], friends:[{_reference:{name:'Miss Piggy'}}] } + for(var k = 0; k < this._arrayOfAllItems.length; ++k){ + var candidateItem = this._arrayOfAllItems[k]; + var found = true; + for(var refKey in referenceDescription){ + if(candidateItem[refKey] != referenceDescription[refKey]){ + found = false; + } + } + if(found){ + arrayOfValues[j] = candidateItem; + } + } + } + if(this.referenceIntegrity){ + var refItem = arrayOfValues[j]; + if(this.isItem(refItem)){ + this._addReferenceToMap(refItem, item, key); + } + } + }else if(this.isItem(value)){ + //It's a child item (not one referenced through _reference). + //We need to treat this as a referenced item, so it can be cleaned up + //in a write store easily. + if(this.referenceIntegrity){ + this._addReferenceToMap(value, item, key); + } + } + } + } + } + } + }, + + _addReferenceToMap: function(/*item*/ refItem, /*item*/ parentItem, /*string*/ attribute){ + // summary: + // Method to add an reference map entry for an item and attribute. + // description: + // Method to add an reference map entry for an item and attribute. // + // refItem: + // The item that is referenced. + // parentItem: + // The item that holds the new reference to refItem. + // attribute: + // The attribute on parentItem that contains the new reference. + + //Stub function, does nothing. Real processing is in ItemFileWriteStore. + }, + + getIdentity: function(/* item */ item){ + console.log("getIdentity(" + item + ")"); + // summary: + // See dojo.data.api.Identity.getIdentity() + var identifier = this._features['dojo.data.api.Identity']; + if(identifier === Number){ + return item[this._itemNumPropName]; // Number + }else{ + var arrayOfValues = item[identifier]; + if(arrayOfValues){ + return arrayOfValues[0]; // Object || String + } + } + return null; // null + }, + + fetchItemByIdentity: function(/* Object */ keywordArgs){ + console.log("fetchItemByIdentity(" + dojo.toJson(keywordArgs)+ ")"); + // summary: + // See dojo.data.api.Identity.fetchItemByIdentity() + + // Hasn't loaded yet, we have to trigger the load. + var item; + var scope; + if(!this._loadFinished){ + var self = this; + if(this._jsonFileUrl){ + + if(this._loadInProgress){ + this._queuedFetches.push({args: keywordArgs}); + }else{ + this._loadInProgress = true; + var getArgs = { + url: self._jsonFileUrl, + handleAs: "json-comment-optional", + preventCache: this.urlPreventCache + }; + var getHandler = dojo.xhrGet(getArgs); + getHandler.addCallback(function(data){ + var scope = keywordArgs.scope?keywordArgs.scope:dojo.global; + try{ + self._getItemsFromLoadedData(data); + self._loadFinished = true; + self._loadInProgress = false; + item = self._getItemByIdentity(keywordArgs.identity); + if(keywordArgs.onItem){ + keywordArgs.onItem.call(scope, item); + } + self._handleQueuedFetches(); + }catch(error){ + self._loadInProgress = false; + if(keywordArgs.onError){ + keywordArgs.onError.call(scope, error); + } + } + }); + getHandler.addErrback(function(error){ + self._loadInProgress = false; + if(keywordArgs.onError){ + var scope = keywordArgs.scope?keywordArgs.scope:dojo.global; + keywordArgs.onError.call(scope, error); + } + }); + } + + }else if(this._jsonData){ + // Passed in data, no need to xhr. + self._getItemsFromLoadedData(self._jsonData); + self._jsonData = null; + self._loadFinished = true; + item = self._getItemByIdentity(keywordArgs.identity); + if(keywordArgs.onItem){ + scope = keywordArgs.scope?keywordArgs.scope:dojo.global; + keywordArgs.onItem.call(scope, item); + } + } + }else{ + // Already loaded. We can just look it up and call back. + item = this._getItemByIdentity(keywordArgs.identity); + if(keywordArgs.onItem){ + scope = keywordArgs.scope?keywordArgs.scope:dojo.global; + keywordArgs.onItem.call(scope, item); + } + } + }, + + _getItemByIdentity: function(/* Object */ identity){ + // summary: + // Internal function to look an item up by its identity map. + var item = null; + if(this._itemsByIdentity){ + item = this._itemsByIdentity[identity]; + }else{ + item = this._arrayOfAllItems[identity]; + } + if(item === undefined){ + item = null; + } + return item; // Object + }, + + getIdentityAttributes: function(/* item */ item){ + console.log("getIdentityAttributes(" + item + ")"); + // summary: + // See dojo.data.api.Identity.getIdentifierAttributes() + + var identifier = this._features['dojo.data.api.Identity']; + if(identifier === Number){ + // If (identifier === Number) it means getIdentity() just returns + // an integer item-number for each item. The dojo.data.api.Identity + // spec says we need to return null if the identity is not composed + // of attributes + return null; // null + }else{ + return [identifier]; // Array + } + }, + + _forceLoad: function(){ + // summary: + // Internal function to force a load of the store if it hasn't occurred yet. This is required + // for specific functions to work properly. + var self = this; + if(this._jsonFileUrl){ + var getArgs = { + url: self._jsonFileUrl, + handleAs: "json-comment-optional", + preventCache: this.urlPreventCache, + sync: true + }; + var getHandler = dojo.xhrGet(getArgs); + getHandler.addCallback(function(data){ + try{ + //Check to be sure there wasn't another load going on concurrently + //So we don't clobber data that comes in on it. If there is a load going on + //then do not save this data. It will potentially clobber current data. + //We mainly wanted to sync/wait here. + //TODO: Revisit the loading scheme of this store to improve multi-initial + //request handling. + if(self._loadInProgress !== true && !self._loadFinished){ + self._getItemsFromLoadedData(data); + self._loadFinished = true; + }else if(self._loadInProgress){ + //Okay, we hit an error state we can't recover from. A forced load occurred + //while an async load was occurring. Since we cannot block at this point, the best + //that can be managed is to throw an error. + throw new Error("openils.DebuggingIFRS: Unable to perform a synchronous load, an async load is in progress."); + } + }catch(e){ + console.log(e); + throw e; + } + }); + getHandler.addErrback(function(error){ + throw error; + }); + }else if(this._jsonData){ + self._getItemsFromLoadedData(self._jsonData); + self._jsonData = null; + self._loadFinished = true; + } + }, + + fetch: function(/* Object? */ request){ + console.log("fetch(" + dojo.toJson(request) + ")"); + // summary: + // The simpleFetch mixin is designed to serve as a set of function(s) that can + // be mixed into other datastore implementations to accelerate their development. + // The simpleFetch mixin should work well for any datastore that can respond to a _fetchItems() + // call by returning an array of all the found items that matched the query. The simpleFetch mixin + // is not designed to work for datastores that respond to a fetch() call by incrementally + // loading items, or sequentially loading partial batches of the result + // set. For datastores that mixin simpleFetch, simpleFetch + // implements a fetch method that automatically handles eight of the fetch() + // arguments -- onBegin, onItem, onComplete, onError, start, count, sort and scope + // The class mixing in simpleFetch should not implement fetch(), + // but should instead implement a _fetchItems() method. The _fetchItems() + // method takes three arguments, the keywordArgs object that was passed + // to fetch(), a callback function to be called when the result array is + // available, and an error callback to be called if something goes wrong. + // The _fetchItems() method should ignore any keywordArgs parameters for + // start, count, onBegin, onItem, onComplete, onError, sort, and scope. + // The _fetchItems() method needs to correctly handle any other keywordArgs + // parameters, including the query parameter and any optional parameters + // (such as includeChildren). The _fetchItems() method should create an array of + // result items and pass it to the fetchHandler along with the original request object + // -- or, the _fetchItems() method may, if it wants to, create an new request object + // with other specifics about the request that are specific to the datastore and pass + // that as the request object to the handler. + // + // For more information on this specific function, see dojo.data.api.Read.fetch() + request = request || {}; + if(!request.store){ + request.store = this; + } + var self = this; + + var _errorHandler = function(errorData, requestObject){ + if(requestObject.onError){ + var scope = requestObject.scope || dojo.global; + requestObject.onError.call(scope, errorData, requestObject); + } + }; + + var _fetchHandler = function(items, requestObject){ + var oldAbortFunction = requestObject.abort || null; + var aborted = false; + + var startIndex = requestObject.start?requestObject.start:0; + var endIndex = (requestObject.count && (requestObject.count !== Infinity))?(startIndex + requestObject.count):items.length; + + requestObject.abort = function(){ + aborted = true; + if(oldAbortFunction){ + oldAbortFunction.call(requestObject); + } + }; + + var scope = requestObject.scope || dojo.global; + if(!requestObject.store){ + requestObject.store = self; + } + if(requestObject.onBegin){ + requestObject.onBegin.call(scope, items.length, requestObject); + } + if(requestObject.sort){ + items.sort(dojo.data.util.sorter.createSortFunction(requestObject.sort, self)); + } + if(requestObject.onItem){ + for(var i = startIndex; (i < items.length) && (i < endIndex); ++i){ + var item = items[i]; + if(!aborted){ + requestObject.onItem.call(scope, item, requestObject); + } + } + } + if(requestObject.onComplete && !aborted){ + var subset = null; + if (!requestObject.onItem) { + subset = items.slice(startIndex, endIndex); + } + requestObject.onComplete.call(scope, subset, requestObject); + } + }; + this._fetchItems(request, _fetchHandler, _errorHandler); + return request; // Object + } +}); + diff --git a/Open-ILS/web/js/dojo/openils/FlattenerStore.js b/Open-ILS/web/js/dojo/openils/FlattenerStore.js new file mode 100644 index 0000000000..7e763c2fd4 --- /dev/null +++ b/Open-ILS/web/js/dojo/openils/FlattenerStore.js @@ -0,0 +1,280 @@ +if (!dojo._hasResource["openils.FlattenerStore"]) { + dojo._hasResource["openils.FlattenerStore"] = true; + + dojo.provide("openils.FlattenerStore"); + + dojo.require("DojoSRF"); + dojo.require("openils.User"); + dojo.require("openils.Util"); + + /* An exception class specific to openils.FlattenerStore */ + function FlattenerStoreError(message) { this.message = message; } + FlattenerStoreError.prototype.toString = function() { + return "openils.FlattenerStore: " + this.message; + }; + + dojo.declare( + "openils.FlattenerStore", null, { + + "_last_fetch": null, /* used internally */ + + /* Everything between here and the constructor can be specified in + * the constructor's args object. */ + + "fmClass": null, + "mapClause": null, + "whereClause": null, + "sloClause": null, + + + "constructor": function(/* object */ args) { + dojo.mixin(this, args); /* XXX do we actually need this? */ + this._current_items = {}; + }, + + "_prepare_flattener_url": function(req) { + var url = "/opac/extras/flattener?"; + + var limit = (!isNaN(req.count) && req.count != Infinity) ? + req.count : this.limit; + + var slo = dojo.clone(this.sloClause); + slo.limit = limit; + + // XXX offset + var parts = [ + "hint=" + this.fmClass, + "ses=" + openils.User.authtoken, + "where=" + dojo.toJson(this.whereClause), + "slo=" + dojo.toJson(slo), + "identifier=" + fieldmapper.IDL.fmclasses[this.fmClass].pkey + ]; + + if (this.mapKey) { /* XXX TODO, get a map key */ + parts.push("key=" + this.mapKey); + } else { + parts.push("map=" + dojo.toJson(this.mapClause)); + } + + return url + parts.join("&"); + }, + + "_display_attributes": function() { + var self = this; + + /* XXX normalize map clause like ML does, + * so we can use short form */ + return openils.Util.objectProperties(this.mapClause).filter( + function(key) { return self.mapClause[key].display; } + ); + }, + /* *** Begin dojo.data.api.Read methods *** */ + + "getValue": function( + /* object */ item, + /* string */ attribute, + /* anything */ defaultValue) { + if (!this.isItem(item)) + throw new FlattenerStoreError("getValue(): bad item " + item); + else if (typeof attribute != "string") + throw new FlattenerStoreError("getValue(): bad attribute"); + + var value = item[attribute]; + return (typeof value == "undefined") ? defaultValue : value; + }, + + "getValues": function(/* object */ item, /* string */ attribute) { + if (!this.isItem(item) || typeof attribute != "string") + throw new FlattenerStoreError("bad arguments"); + + var result = this.getValue(item, attribute, []); + return dojo.isArray(result) ? result : [result]; + }, + + "getAttributes": function(/* object */ item) { + if (!this.isItem(item)) + throw new FlattenerStoreError("getAttributes(): bad args"); + else + return this._display_attributes(); + }, + + "hasAttribute": function(/* object */ item, /* string */ attribute) { + if (!this.isItem(item) || typeof attribute != "string") { + throw new FlattenerStoreError("hasAttribute(): bad args"); + } else { + return dojo.indexOf(this._display_attributes(), attribute) > -1; + } + }, + + "containsValue": function( + /* object */ item, + /* string */ attribute, + /* anything */ value) { + if (!this.isItem(item) || typeof attribute != "string") + throw new FlattenerStoreError("bad data"); + else + return ( + dojo.indexOf(this.getValues(item, attribute), value) >= -1 + ); + }, + + /* =========== not dealt with at all below here yet ============ */ + + "isItem": function(/* anything */ something) { + if (typeof something != "object" || something === null) + return false; + + var fields = + + for (var i = 0; i < _autosuggest_fields.length; i++) { + var cur = _autosuggest_fields[i]; + if (typeof something[cur] == "undefined") + return false; + } + return true; + }, + + "isItemLoaded": function(/* anything */ something) { + return this.isItem(something); /* for this store, + items are always loaded */ + }, + + "close": function(/* object */ request) { /* no-op */ return; }, + "getLabel": function(/* object */ item) { return "match"; }, + "getLabelAttributes": function(/* object */ item) { return ["match"]; }, + + "loadItem": function(/* object */ keywordArgs) { + if (!this.isItem(keywordArgs.item)) + throw new FlattenerStoreError("not an item; can't load it"); + + keywordArgs.identity = this.getIdentity(item); + return this.fetchItemByIdentity(keywordArgs); + }, + + "fetch": function(/* request-object */ req) { + // Respect the following properties of the *req* object: + // + // query a dojo-style query, which will need modest + // translation for our server-side service + // count an int + // onBegin a callback that takes the number of items + // that this call to fetch() will return, but + // we always give it -1 (i.e. unknown) + // onItem a callback that takes each item as we get it + // onComplete a callback that takes the list of items + // after they're all fetched + // + // The onError callback is ignored for now (haven't thought + // of anything useful to do with it yet). + // + // The Read API also charges this method with adding an abort + // callback to the *req* object for the caller's use, but + // the one we provide does nothing but issue an alert(). + + if (!this.cm_cache.is_done) { + if (typeof req.onComplete == "function") + req.onComplete.call(callback_scope, [], req); + return; + } + this._current_items = {}; + + var callback_scope = req.scope || dojo.global; + var url = this._prepare_flattener_url(req); + + if (!url) { + if (typeof req.onComplete == "function") + req.onComplete.call(callback_scope, [], req); + return; + } + + var self = this; + var process_fetch = function(obj, when) { + if (when < self._last_fetch) /* Stale response. Discard. */ + return; + + dojo.forEach( + obj.val, + function(item) { + item.id = item.field + "_" + item.term; + + self._current_items[item.id] = item; + + if (typeof req.onItem == "function") + req.onItem.call(callback_scope, item, req); + } + ); + + if (typeof req.onComplete == "function") { + req.onComplete.call( + callback_scope, + openils.Util.objectValues(self._current_items), + req + ); + } + }; + + req.abort = function() { + alert("The 'abort' operation is not supported"); + }; + + if (typeof req.onBegin == "function") + req.onBegin.call(callback_scope, -1, req); + + var fetch_time = this._last_fetch = (new Date().getTime()); + + dojo.xhrGet({ + "url": url, + "handleAs": "json", + "sync": false, + "preventCache": true, + "headers": {"Accept": "application/json"}, + "load": function(obj) { process_fetch(obj, fetch_time); } + }); + + /* as for onError: what to do? */ + + return req; + }, + + /* *** Begin dojo.data.api.Identity methods *** */ + + "getIdentity": function(/* object */ item) { + if (!this.isItem(item)) + throw new FlattenerStoreError("not an item"); + + return item.id; + }, + + "getIdentityAttributes": function(/* object */ item) { return ["id"]; }, + + "fetchItemByIdentity": function(/* object */ keywordArgs) { + if (keywordArgs.identity == undefined) + return null; // Identity API spec unclear whether error callback + // would need to be run, so we won't. + var callback_scope = keywordArgs.scope || dojo.global; + + var item; + if (item = this._current_items[keywordArgs.identity]) { + if (typeof keywordArgs.onItem == "function") + keywordArgs.onItem.call(callback_scope, item); + + return item; + } else { + if (typeof keywordArgs.onError == "function") + keywordArgs.onError.call(callback_scope, E); + + return null; + } + }, + + /* *** Classes implementing any Dojo APIs do this to list which + * APIs they're implementing. *** */ + + "getFeatures": function() { + return { + "dojo.data.api.Read": true, + "dojo.data.api.Identity": true + }; + } + }); +} diff --git a/Open-ILS/web/js/dojo/openils/widget/FlattenerGrid.js b/Open-ILS/web/js/dojo/openils/widget/FlattenerGrid.js new file mode 100644 index 0000000000..a6215cbc0e --- /dev/null +++ b/Open-ILS/web/js/dojo/openils/widget/FlattenerGrid.js @@ -0,0 +1,14 @@ +if (!dojo._hasResource["openils.widget.FlattenerGrid"]) { + dojo.provide("openils.widget.FlattenerGrid"); + + dojo.require("dojox.grid.DataGrid"); + dojo.require("dijit.form.Button"); + dojo.require("dijit.layout.ContentPane"); + + dojo.declare( + "openils.widget.FlattenerGrid", + [dojox.grid.DataGrid], { + "nothing": null + } + ); +} -- 2.11.0