LP#1750894 Cascade settings auto-magic migration
authorBill Erickson <berickxx@gmail.com>
Thu, 24 May 2018 16:24:29 +0000 (12:24 -0400)
committerBill Erickson <berickxx@gmail.com>
Tue, 29 May 2018 14:13:36 +0000 (10:13 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/src/templates/staff/admin/workstation/t_stored_prefs.tt2
Open-ILS/web/js/ui/default/staff/admin/workstation/app.js
Open-ILS/web/js/ui/default/staff/circ/checkin/app.js
Open-ILS/web/js/ui/default/staff/services/hatch.js

index e4c34c9..6722748 100644 (file)
@@ -20,42 +20,6 @@ Click on the delete (X) button to remove a preference value.
       </div>
     </div>
   </div>
-
-  <div class="row">
-    <div class="col-md-12">
-      <div class="panel panel-info">
-        <div class="panel-heading">
-          <h3 class="panel-title">
-            [% l('Experimental: Migrate Workstation Settings to Server') %]
-          </h3>
-        </div>
-        <div class="panel-body">
-          <p>
-[% | l %]Settings may be migrated to the server using the first option below 
-as many times as needed for testing and verification.  Once local settings 
-are deleted via the second option, no settings will remain to migrate, unless
-they are added back through some external mechanism.[% END %]
-          </p>
-          <p>
-[% | l %]Note that just because a setting exists under "Server Prefs" below
-does not mean it has been migrated.  Check the value by clicking on the 
-setting name to be sure.[% END %]
-          </p>
-          <div class="row pad-left">
-            <button class="btn btn-success pad-right" 
-              ng-click="migrateServerSettings()">
-              [% l('#1 Migrate and Keep Local Settings (Testing)') %]
-            </button>
-            <button class="btn btn-warning pad-left" 
-              ng-click="migrateServerSettings(true)">
-              [% l('#2 Migrate and Delete Local Settings') %]
-            </button>
-          </div>
-        </div>
-      </div>
-    </div>
-  </div>
-
   <div class="row">
     <div class="col-md-4">
 
index fa0df89..893a2dc 100644 (file)
@@ -804,33 +804,6 @@ function($scope , $q , egCore , egConfirmDialog , egProgressDialog , $timeout) {
             }
         );
     }
-
-    $scope.migrateServerSettings = function(deleteLocal) {
-
-        var promise = !deleteLocal ? $q.when() : 
-            egConfirmDialog.open(
-                egCore.strings.HATCH_SERVER_SETTINGS_MIGRATION_CONFIRM, '', {}
-            ).result;
-
-        promise.then(function() {
-            egProgressDialog.open();
-            // timeout added because closing a progress dialog too quickly
-            // (before it's registered that it's opened) can leave the
-            // dialog stranded open.  This is only an issue when no local
-            // settings data exists to migrate, causing the migration to 
-            // be instantaneous.
-            $timeout(function() {
-                egCore.hatch.migrateServerSettings(deleteLocal).then(
-                    function() {},
-                    function() {},
-                    function() {
-                        egProgressDialog.increment();
-                    }
-                )['finally'](egProgressDialog.close);
-            }, 100);
-        });
-    }
-
 }])
 
 .controller('WSRegCtrl',
index ef3944f..7eb8cf0 100644 (file)
@@ -49,6 +49,7 @@ function($scope , $q , $window , $location , $timeout , egCore , checkinSvc , eg
     $scope.grid_persist_key = $scope.is_capture ? 
         'circ.checkin.capture' : 'circ.checkin.checkin';
 
+    // TODO: add this to the setting batch lookup below
     egCore.hatch.getItem('circ.checkin.strict_barcode')
         .then(function(sb){ $scope.strict_barcode = sb });
 
@@ -88,9 +89,18 @@ function($scope , $q , $window , $location , $timeout , egCore , checkinSvc , eg
     }
 
     // set modifiers from stored preferences
-    angular.forEach(modifiers, function(mod) {
-        egCore.hatch.getItem('eg.circ.checkin.' + mod)
-        .then(function(val) { if (val) $scope.modifiers[mod] = true });
+    // We know these settings live on the server, so manually batch
+    // them for speed.
+    // TODO: once Hatch storage is deprecated, add a getItemBatch function.
+    var snames = modifiers.map(function(m) {return 'eg.circ.checkin.' + m;});
+    egCore.hatch.getServerItemBatch(snames).then(function(settings) {
+        angular.forEach(settings, function(val, key) {
+            if (val === true) {
+                var parts = key.split('.')
+                var mod = parts.pop();
+                $scope.modifiers[mod] = true;
+            }
+        })
     });
 
     // set / unset a checkin modifier
index c360b79..cd97e3e 100644 (file)
@@ -39,6 +39,10 @@ angular.module('egCoreMod')
     // 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
@@ -67,47 +71,30 @@ angular.module('egCoreMod')
     /**
      * Settings with these prefixes will always live in the browser.
      */
-    service.browserOnlyPrefixes = ['eg.workstation', 'eg.hatch'];
-
-    // For testing (and possibly migration) purposes, hard-code the
-    // list of settings that are available to be applied on the server,
-    // either via workstation or user settings.  Ultimately, the 
-    // assumption will be that all settings live on the server except
-    // those specified by service.browserOnlyPrefixes.
-    service.serverSettings = [
-        'eg.circ.checkin.no_precat_alert',
-        'eg.circ.checkin.noop',
-        'eg.circ.checkin.void_overdues',
-        'eg.circ.checkin.auto_print_holds_transits',
-        'eg.circ.checkin.clear_expired',
-        'eg.circ.checkin.retarget_holds',
-        'eg.circ.checkin.retarget_holds_all',
-        'eg.circ.checkin.hold_as_transit',
-        'eg.circ.checkin.manual_float',
-        'circ.checkin.strict_barcode',
-        'eg.circ.patron.summary.collapse',
-        'circ.bills.receiptonpay',
-        'circ.renew.strict_barcode',
-        'cat.holdings_show_copies',
-        'cat.holdings_show_empty',
-        'cat.holdings_show_vols'
+    service.browserOnlyPrefixes = [
+        'eg.workstation', 
+        'eg.hatch',
+        'eg.cache',
+        'current_tag_table_marc21_biblio',
+        'FFPosTable_REC',
+        'FFValueTable_REC'
     ];
 
-    service.keyStoredOnServer = function(key) {
+    service.keyStoredInBrowser = function(key) {
 
         if (service.disableServerSettings) {
-            return false;
+            // When server-side storage is disabled, treat every
+            // setting like it's stored locally.
+            return true;
         }
 
         var browserOnly = false;
-        angular.forEach(service.browserOnlyPrefixes, function(pfx) {
+        service.browserOnlyPrefixes.forEach(function(pfx) {
             if (key.match(new RegExp('^' + pfx))) 
                 browserOnly = true;
         });
 
-        if (browserOnly) return false;
-
-        return service.serverSettings.indexOf(key) > -1
+        return browserOnly;
     }
 
     // write a message to the Hatch port
@@ -261,23 +248,37 @@ angular.module('egCoreMod')
     // get the value for a stored item
     service.getItem = function(key) {
 
-        if (service.keyStoredOnServer(key))
+        if (!service.keyStoredInBrowser(key)) {
             return service.getServerItem(key);
+        }
 
-        if (!service.useSettings())
-            return $q.when(service.getLocalItem(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;
+    }
+
+    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();
     }
 
@@ -296,7 +297,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) {
@@ -325,38 +326,75 @@ angular.module('egCoreMod')
      */
     service.setItem = function(key, value) {
 
-        if (service.keyStoredOnServer(key))
+        if (!service.keyStoredInBrowser(key)) {
             return service.setServerItem(key, value);
+        }
 
-        if (!service.useSettings())
-            return $q.when(service.setLocalItem(key, value));
+        var deferred = $q.defer();
+        service.setBrowserItem(key, value).then(
+            function(val) {deferred.resolve(val);},
 
-        if (service.hatchAvailable)
-            return service.setRemoteItem(key, value);
+            function() { // Hatch error
 
-        if (service.keyIsOnCall(key)) {
-            console.warn("Unable to setItem in Hatch: " + 
-                key + ". Setting in local storage instead");
+                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_user_setting() === 'f' && 
+            summary.has_workstation_setting() === 'f') {
+
+            console.warn('No server setting type exists for ' + key);
+            service.setLocalItem(key, value); 
+            return $q.when();
+        }
+
         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(settings) {
-            return service.keyCache[key] = value;
+        ).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;
         });
     }
 
@@ -373,85 +411,58 @@ angular.module('egCoreMod')
             'open-ils.actor.settings.retrieve.atomic',
             [key], service.auth.token()
         ).then(function(settings) {
-            var val = settings[0].value();
-            // The server returns null for undefined settings.
-            // Treat as undefined for backwards compat.
-            service.keyCache[key] = (val === null) ? undefined : val;
-            return service.keyCache[key];
+            return service.handleServerItemResponse(settings[0]);
         });
     }
 
-    service.migrateServerSettings = function(deleteLocal) {
-
-        if (!service.auth) service.auth = $injector.get('egAuth');
-        if (!service.auth.token()) return $q.reject('no authtoken');
+    service.handleServerItemResponse = function(summary) {
+        var key = summary.name();
+        var val = summary.value();
 
-        var deferred = $q.defer();
-        var settings = service.serverSettings.slice(0); // clone
+        summary.value(null); // avoid duplicate value caches
+        service.serverSettingSummaries[key] = summary;
 
-        // Allow get/remove calls to fall back to local options
-        // during migration.
-        service.disableServerSettings = true;
+        if (val !== null) {
+            // We have a server setting.  Nothing left to do.
+            return $q.when(service.keyCache[key] = val);
+        }
 
-        function migrateNext(key) {
+        if (summary.has_user_setting() === 'f' &&
+            summary.has_workstation_setting() === 'f') {
 
-            if (!key) {
-                service.disableServerSettings = false;
-                return deferred.resolve();
-            }
+            console.warn('No server setting type exists for ' 
+                + key + ', using local value.');
 
-            service.migrateOneServerSetting(key, deleteLocal).then(
-                function() {
-                    deferred.notify('migrated ' + key);
-                    migrateNext(settings.shift());
-                },
-                function() {
-                    service.disableServerSettings = false;
-                    deferred.reject(
-                        'Something failed during settings migration');
-                }
-            );
+            return service.getBrowserItem(key);
         }
 
-        migrateNext(settings.shift());
-        return deferred.promise;
-    }
+        // A server setting type exists, but no server value exists.
+        // See if we can migrate a local setting to the server.
 
-    service.migrateOneServerSetting = function(key, deleteLocal) {
         var deferred = $q.defer();
+        service.getBrowserItem(key).then(function(browserVal) {
 
-        service.getItem(key).then(function(value) {
-
-            if (value === undefined || value === null) {
-                console.log(key + ' has no value to migrate');
-                // Nothing to migrate.
-                if (deleteLocal) {
-                    // for good measure.
-                    service.removeItem(key); 
-                }
-                return deferred.resolve();
+            if (browserVal === null && browserVal === undefined) {
+                // No local value to migrate.
+                return deferred.resolve(service.keyCache[key] = undefined);
             }
 
-            service.setServerItem(key, value).then(
-                function() {
-                    console.debug(
-                        'setting ' + key + ' successfully stored on server');
-
-                    if (!deleteLocal) {
-                        return deferred.resolve();
+            // 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');
                     }
-
-                    service.removeItem(key).then(function() {
-                        console.debug(
-                            'setting ' + key + ' removed from workstation');
-                        deferred.resolve();
-                    });
-                },
-                function() {
-                    deferred.reject('applying server setting ' + key);
+                    deferred.resolve(service.keyCache[key] = undefined);
                 }
             );
-        })
+        });
 
         return deferred.promise;
     }
@@ -473,7 +484,7 @@ angular.module('egCoreMod')
             function(setting) {
                 var val = setting.value();
                 // The server returns null for undefined settings.
-                // Treat as undefined for backwards compat.
+                // Treat as undefined locally for backwards compat.
                 service.keyCache[setting.name()] = 
                     foundValues[setting.name()] = 
                     (val === null) ? undefined : val;
@@ -497,8 +508,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);
     }
 
@@ -563,24 +577,40 @@ angular.module('egCoreMod')
 
     // remove a stored item
     service.removeItem = function(key) {
-        if (service.keyStoredOnServer(key))
+
+        if (!service.keyStoredInBrowser(key)) {
             return service.removeServerItem(key);
+        }
 
-        if (!service.useSettings())
-            return $q.when(service.removeLocalItem(key));
+        var deferred = $q.defer();
+        service.removeBrowserItem(key).then(
+            function(response) {deferred.resolve(response);},
+            function() { // Hatch error
 
-        if (service.hatchAvailable) 
-            return service.removeRemoteItem(key);
+                if (service.keyIsOnCall(key)) {
+                    console.warn("Unable to removeItem from Hatch: " + key + 
+                        ". Removing item from local storage instead");
 
-        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) {