LP#1750894 Workstation & Cascade settings
[working/Evergreen.git] / Open-ILS / web / js / ui / default / staff / services / hatch.js
index f7083a7..d35c3f1 100644 (file)
 angular.module('egCoreMod')
 
 .factory('egHatch',
-           ['$q','$window','$timeout','$interpolate','$cookies',
-    function($q , $window , $timeout , $interpolate , $cookies) {
+           ['$q','$window','$timeout','$interpolate','$cookies','egNet','$injector',
+    function($q , $window , $timeout , $interpolate , $cookies , egNet , $injector ) {
 
     var service = {};
     service.msgId = 1;
     service.messages = {};
     service.hatchAvailable = false;
+    service.auth = null;  // ref to egAuth loaded on-demand to avoid circular ref.
+    service.disableServerSettings = false;
 
     // key/value cache -- avoid unnecessary Hatch extension requests.
     // Only affects *RemoteItem calls.
     service.keyCache = {}; 
 
+    // Keep a local copy of all retrieved setting summaries, which indicate
+    // which setting types exist for each setting.  
+    service.serverSettingSummaries = {};
+
     /**
      * List string prefixes for On-Call storage keys. On-Call keys
      * are those that can be set/get/remove'd from localStorage when
@@ -50,7 +56,7 @@ angular.module('egCoreMod')
      * at a time and each maintains its own data separately.
      */
     service.onCallPrefixes = ['eg.workstation'];
-
+    
     // Returns true if the key can be set/get in localStorage even when 
     // Hatch is not available.
     service.keyIsOnCall = function(key) {
@@ -62,6 +68,35 @@ angular.module('egCoreMod')
         return oncall;
     }
 
+    /**
+     * Settings with these prefixes will always live in the browser.
+     */
+    service.browserOnlyPrefixes = [
+        'eg.workstation', 
+        'eg.hatch',
+        'eg.cache',
+        'current_tag_table_marc21_biblio',
+        'FFPos',
+        'FFValue'
+    ];
+
+    service.keyStoredInBrowser = function(key) {
+
+        if (service.disableServerSettings) {
+            // When server-side storage is disabled, treat every
+            // setting like it's stored locally.
+            return true;
+        }
+
+        var browserOnly = false;
+        service.browserOnlyPrefixes.forEach(function(pfx) {
+            if (key.match(new RegExp('^' + pfx))) 
+                browserOnly = true;
+        });
+
+        return browserOnly;
+    }
+
     // write a message to the Hatch port
     service.sendToHatch = function(msg) {
         var msg2 = {};
@@ -198,6 +233,8 @@ angular.module('egCoreMod')
         );
     }
 
+    // TODO: once Hatch is printing-only, should probably store
+    // this preference on the server.
     service.usePrinting = function() {
         return service.getLocalItem('eg.hatch.enable.printing');
     }
@@ -213,20 +250,77 @@ angular.module('egCoreMod')
     // get the value for a stored item
     service.getItem = function(key) {
 
-        if (!service.useSettings())
-            return $q.when(service.getLocalItem(key));
+        if (!service.keyStoredInBrowser(key)) {
+            return service.getServerItem(key);
+        }
+
+        var deferred = $q.defer();
 
-        if (service.hatchAvailable) 
-            return service.getRemoteItem(key);
+        service.getBrowserItem(key).then(
+            function(val) { deferred.resolve(val); },
 
-        if (service.keyIsOnCall(key)) {
-            console.warn("Unable to getItem from Hatch: " + key + 
-                ". Retrieving item from local storage instead");
+            function() { // Hatch error
+                if (service.keyIsOnCall(key)) {
+                    console.warn("Unable to getItem from Hatch: " + key + 
+                        ". Retrieving item from local storage instead");
+                    deferred.resolve(service.getLocalItem(key));
+                }
+
+                deferred.reject("Unable to getItem from Hatch: " + key);
+            }
+        );
+
+        return deferred.promise;
+    }
+
+    // Collect values in batch.
+    // For server-stored values espeically, this is more efficient 
+    // than a series of one-off calls.
+    service.getItemBatch = function(keys) {
+        var browserKeys = [];
+        var serverKeys = [];
+
+        // To take full advantage of the getServerItemBatch call,
+        // we have to know in advance which keys to send to the server
+        // vs those to handle in the browser.
+        keys.forEach(function(key) {
+            if (service.keyStoredInBrowser(key)) {
+                browserKeys.push(key);
+            } else {
+                serverKeys.push(key);
+            }
+        });
+
+        var settings = {};
+
+        var serverPromise = serverKeys.length === 0 ? $q.when() : 
+            service.getServerItemBatch(serverKeys).then(function(values) {
+                angular.forEach(values, function(val, key) {
+                    settings[key] = val;
+                });
+            });
+
+        var browserPromises = [];
+        browserKeys.forEach(function(key) {
+            browserPromises.push(
+                service.getBrowserItem(key).then(function(val) {
+                    settings[key] = val;
+                })
+            );
+        });
+
+        return $q.all(browserPromises.concat(serverPromise))
+            .then(function() {return settings});
+    }
 
+    service.getBrowserItem = function(key) {
+        if (service.useSettings()) {
+            if (service.hatchAvailable) {
+                return service.getRemoteItem(key);
+            }
+        } else {
             return $q.when(service.getLocalItem(key));
         }
-
-        console.error("Unable to getItem from Hatch: " + key);
         return $q.reject();
     }
 
@@ -245,7 +339,7 @@ angular.module('egCoreMod')
 
     service.getLocalItem = function(key) {
         var val = $window.localStorage.getItem(key);
-        if (val == null) return;
+        if (val === null || val === undefined) return;
         try {
             return JSON.parse(val);
         } catch(E) {
@@ -273,23 +367,200 @@ angular.module('egCoreMod')
      * tmp values are removed during logout or browser close.
      */
     service.setItem = function(key, value) {
-        if (!service.useSettings())
-            return $q.when(service.setLocalItem(key, value));
 
-        if (service.hatchAvailable)
-            return service.setRemoteItem(key, value);
+        if (!service.keyStoredInBrowser(key)) {
+            return service.setServerItem(key, value);
+        }
 
-        if (service.keyIsOnCall(key)) {
-            console.warn("Unable to setItem in Hatch: " + 
-                key + ". Setting in local storage instead");
+        var deferred = $q.defer();
+        service.setBrowserItem(key, value).then(
+            function(val) {deferred.resolve(val);},
+
+            function() { // Hatch error
+
+                if (service.keyIsOnCall(key)) {
+                    console.warn("Unable to setItem in Hatch: " + 
+                        key + ". Setting in local storage instead");
+
+                    deferred.resolve(service.setLocalItem(key, value));
+                }
+                deferred.reject("Unable to setItem in Hatch: " + key);
+            }
+        );
+    }
 
+    service.setBrowserItem = function(key, value) {
+        if (service.useSettings()) {
+            if (service.hatchAvailable) {
+                return service.setRemoteItem(key, value);
+            } else {
+                return $q.reject('Unable to get item from hatch');
+            }
+        } else {
             return $q.when(service.setLocalItem(key, value));
         }
+    }
 
-        console.error("Unable to setItem in Hatch: " + key);
-        return $q.reject();
+    service.setServerItem = function(key, value) {
+        if (!service.auth) service.auth = $injector.get('egAuth');
+        if (!service.auth.token()) return $q.when();
+
+        // If we have already attempted to retrieve a value for this
+        // setting, then we can tell up front whether applying a value
+        // at the server will be an option.  If not, store locally.
+        var summary = service.serverSettingSummaries[key];
+        if (summary && !summary.has_staff_setting) {
+
+            if (summary.has_org_setting === 't') {
+                // When no user/ws setting types exist but an org unit
+                // setting type does, it means the value cannot be
+                // applied by an individual user.  Nothing left to do.
+                return $q.when();
+            }
+
+            // No setting types of any flavor exist.
+            // Fall back to local storage.
+
+            if (value === null) {
+                // a null value means clear the server setting.
+                return service.removeBrowserItem(key);
+            } else {
+                console.warn('No server setting type exists for ' + key);
+                return service.setBrowserItem(key, value); 
+            }
+        }
+
+        var settings = {};
+        settings[key] = value;
+
+        return egNet.request(
+            'open-ils.actor',
+            'open-ils.actor.settings.apply.user_or_ws',
+            service.auth.token(), settings
+        ).then(function(appliedCount) {
+
+            if (appliedCount == 0) {
+                console.warn('No server setting type exists for ' + key);
+                // We were unable to store the setting on the server,
+                // presumably becuase no server-side setting type exists.
+                // Add to local storage instead.
+                service.setLocalItem(key, value);
+            }
+
+            service.keyCache[key] = value;
+            return appliedCount;
+        });
     }
 
+    service.getServerItem = function(key) {
+        if (key in service.keyCache) {
+            return $q.when(service.keyCache[key])
+        }
+
+        if (!service.auth) service.auth = $injector.get('egAuth');
+        if (!service.auth.token()) return $q.when(null);
+
+        return egNet.request(
+            'open-ils.actor',
+            'open-ils.actor.settings.retrieve.atomic',
+            [key], service.auth.token()
+        ).then(function(settings) {
+            return service.handleServerItemResponse(settings[0]);
+        });
+    }
+
+    service.handleServerItemResponse = function(summary) {
+        var key = summary.name;
+        var val = summary.value;
+
+        // For our purposes, we only care if a setting can be stored
+        // as an org setting or a user-or-workstation setting.
+        summary.has_staff_setting = (
+            summary.has_user_setting === 't' || 
+            summary.has_workstation_setting === 't'
+        );
+
+        summary.value = null; // avoid duplicate value caches
+        service.serverSettingSummaries[key] = summary;
+
+        if (val !== null) {
+            // We have a server setting.  Nothing left to do.
+            return $q.when(service.keyCache[key] = val);
+        }
+
+        if (!summary.has_staff_setting) {
+
+            if (summary.has_org_setting === 't') {
+                // An org unit setting type exists but no value is applied
+                // that this workstation has access to.  The existence of 
+                // an org unit setting type and no user/ws setting type 
+                // means applying a value locally is not allowed.  
+                return $q.when(service.keyCache[key] = undefined);
+            }
+
+            console.warn('No server setting type exists for ' 
+                + key + ', using local value.');
+
+            return service.getBrowserItem(key);
+        }
+
+        // A user/ws setting type exists, but no server value exists.
+        // Migrate the local setting to the server.
+
+        var deferred = $q.defer();
+        service.getBrowserItem(key).then(function(browserVal) {
+
+            if (browserVal === null || browserVal === undefined) {
+                // No local value to migrate.
+                return deferred.resolve(service.keyCache[key] = undefined);
+            }
+
+            // Migrate the local value to the server.
+
+            service.setServerItem(key, browserVal).then(
+                function(appliedCount) {
+                    if (appliedCount == 1) {
+                        console.info('setting ' + key + ' successfully ' +
+                            'migrated to a server setting');
+                        service.removeBrowserItem(key); // fire & forget
+                    } else {
+                        console.error('error migrating setting to server,' 
+                            + ' falling back to local value');
+                    }
+                    deferred.resolve(service.keyCache[key] = browserVal);
+                }
+            );
+        });
+
+        return deferred.promise;
+    }
+
+    service.getServerItemBatch = function(keys) {
+        // no cache checking for now.  assumes batch mode is only
+        // called once on page load.  maybe add cache checking later.
+        if (!service.auth) service.auth = $injector.get('egAuth');
+        if (!service.auth.token()) return $q.when({});
+
+        var foundValues = {};
+        return egNet.request(
+            'open-ils.actor',
+            'open-ils.actor.settings.retrieve',
+            keys, service.auth.token()
+        ).then(
+            function() { return foundValues; }, 
+            function() {},
+            function(setting) {
+                var val = setting.value;
+                // The server returns null for undefined settings.
+                // Treat as undefined locally for backwards compat.
+                service.keyCache[setting.name] = 
+                    foundValues[setting.name] = 
+                    (val === null) ? undefined : val;
+            }
+        );
+    }
+
+
     // set the value for a stored or new item
     service.setRemoteItem = function(key, value) {
         service.keyCache[key] = value;
@@ -305,8 +576,11 @@ angular.module('egCoreMod')
     // If the value is raw, pass it as 'value'.  If it was
     // externally JSONified, pass it via jsonified.
     service.setLocalItem = function(key, value, jsonified) {
-        if (jsonified === undefined ) 
+        if (jsonified === undefined ) {
             jsonified = JSON.stringify(value);
+        } else if (value === undefined) {
+            return;
+        }
         $window.localStorage.setItem(key, jsonified);
     }
 
@@ -371,21 +645,44 @@ angular.module('egCoreMod')
 
     // remove a stored item
     service.removeItem = function(key) {
-        if (!service.useSettings())
-            return $q.when(service.removeLocalItem(key));
 
-        if (service.hatchAvailable) 
-            return service.removeRemoteItem(key);
+        if (!service.keyStoredInBrowser(key)) {
+            return service.removeServerItem(key);
+        }
 
-        if (service.keyIsOnCall(key)) {
-            console.warn("Unable to removeItem from Hatch: " + key + 
-                ". Removing item from local storage instead");
+        var deferred = $q.defer();
+        service.removeBrowserItem(key).then(
+            function(response) {deferred.resolve(response);},
+            function() { // Hatch error
+
+                if (service.keyIsOnCall(key)) {
+                    console.warn("Unable to removeItem from Hatch: " + key + 
+                        ". Removing item from local storage instead");
 
+                    deferred.resolve(service.removeLocalItem(key));
+                }
+
+                deferred.reject("Unable to removeItem from Hatch: " + key);
+            }
+        );
+
+        return deferred.promise;
+    }
+
+    service.removeBrowserItem = function(key) {
+        if (service.useSettings()) {
+            if (service.hatchAvailable) {
+                return service.removeRemoteItem(key);
+            } else {
+                return $q.reject('error talking to Hatch');
+            }
+        } else {
             return $q.when(service.removeLocalItem(key));
         }
+    }
 
-        console.error("Unable to removeItem from Hatch: " + key);
-        return $q.reject();
+    service.removeServerItem = function(key) {
+        return service.setServerItem(key, null);
     }
 
     service.removeRemoteItem = function(key) {
@@ -423,9 +720,12 @@ angular.module('egCoreMod')
 
     // if set, prefix limits the return set to keys starting with 'prefix'
     service.getKeys = function(prefix) {
-        if (service.useSettings()) 
-            return service.getRemoteKeys(prefix);
-        return $q.when(service.getLocalKeys(prefix));
+        var promise = service.getServerKeys(prefix);
+        return service.getBrowserKeys(prefix).then(function(browserKeys) {
+            return promise.then(function(serverKeys) {
+                return serverKeys.concat(browserKeys);
+            });
+        });
     }
 
     service.getRemoteKeys = function(prefix) {
@@ -435,6 +735,22 @@ angular.module('egCoreMod')
         });
     }
 
+    service.getBrowserKeys = function(prefix) {
+        if (service.useSettings()) 
+            return service.getRemoteKeys(prefix);
+        return $q.when(service.getLocalKeys(prefix));
+    }
+
+    service.getServerKeys = function(prefix) {
+        if (!service.auth) service.auth = $injector.get('egAuth');
+        if (!service.auth.token()) return $q.when({});
+        return egNet.request(
+            'open-ils.actor',
+            'open-ils.actor.settings.staff.applied.names.authoritative.atomic',
+            service.auth.token(), prefix
+        );
+    }
+
     service.getLocalKeys = function(prefix) {
         var keys = [];
         var idx = 0;