[open-ils-commits] [GIT] Evergreen ILS branch master updated. beee20c5b0a63bf8248a630686f2fb262d4789ac

Evergreen Git git at git.evergreen-ils.org
Fri Aug 3 13:30:52 EDT 2018


This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "Evergreen ILS".

The branch, master has been updated
       via  beee20c5b0a63bf8248a630686f2fb262d4789ac (commit)
       via  d423090e2277daa2e69f49cef91f37235184db95 (commit)
       via  73b7ace22996370f5bc8e338f115a3d9dd03b349 (commit)
       via  29f35dcc1862db26f486e9e0c6a2c0174281019b (commit)
       via  a61fc24476f6c24bceea2a4b0b9cc900ac0fe735 (commit)
       via  edd1e288917ddde1cebedf3df57732af5fc86d8a (commit)
       via  14803bb75f6129d59c3dc74e9c2df5909348969e (commit)
       via  f06c3c2110e164b01a46014972258ba41423aa7a (commit)
       via  ff6aa2ab53271740a0b2fd73859af785f7765f32 (commit)
       via  7790968d6410a3de13ed606150b61f148f96b2bc (commit)
       via  7891c0241e9b5273f122f8d77cb20ebbf2d5cf6b (commit)
      from  3bc28aedbe392efaf8d6930deebb898c1406b0f9 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit beee20c5b0a63bf8248a630686f2fb262d4789ac
Author: Kathy Lussier <klussier at masslnc.org>
Date:   Fri Aug 3 13:29:14 2018 -0400

    LP#1750894: Stamping upgrade script for workstation settings on server
    
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/sql/Pg/002.schema.config.sql b/Open-ILS/src/sql/Pg/002.schema.config.sql
index 31cd398..08763f6 100644
--- a/Open-ILS/src/sql/Pg/002.schema.config.sql
+++ b/Open-ILS/src/sql/Pg/002.schema.config.sql
@@ -92,7 +92,7 @@ CREATE TRIGGER no_overlapping_deps
     BEFORE INSERT OR UPDATE ON config.db_patch_dependencies
     FOR EACH ROW EXECUTE PROCEDURE evergreen.array_overlap_check ('deprecates');
 
-INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1115', :eg_version); -- miker/berick
+INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1117', :eg_version); -- berick/kmlussier
 
 CREATE TABLE config.bib_source (
 	id		SERIAL	PRIMARY KEY,
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.workstation-settings.sql b/Open-ILS/src/sql/Pg/upgrade/1116.schema.workstation-settings.sql
similarity index 99%
rename from Open-ILS/src/sql/Pg/upgrade/XXXX.schema.workstation-settings.sql
rename to Open-ILS/src/sql/Pg/upgrade/1116.schema.workstation-settings.sql
index 11ff097..fd2e16a 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.workstation-settings.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/1116.schema.workstation-settings.sql
@@ -8,7 +8,7 @@ CREATE TYPE actor.cascade_setting_summary AS (
     has_workstation_setting BOOLEAN
 );
 
--- SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+SELECT evergreen.upgrade_deps_block_check('1116', :eg_version);
 
 CREATE TABLE config.workstation_setting_type (
     name            TEXT    PRIMARY KEY,
diff --git a/Open-ILS/src/sql/Pg/upgrade/YYYY.data.workstation-settings.sql b/Open-ILS/src/sql/Pg/upgrade/1117.data.workstation-settings.sql
similarity index 99%
rename from Open-ILS/src/sql/Pg/upgrade/YYYY.data.workstation-settings.sql
rename to Open-ILS/src/sql/Pg/upgrade/1117.data.workstation-settings.sql
index ba841cd..1b99f9d 100644
--- a/Open-ILS/src/sql/Pg/upgrade/YYYY.data.workstation-settings.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/1117.data.workstation-settings.sql
@@ -1,5 +1,7 @@
 BEGIN;
 
+SELECT evergreen.upgrade_deps_block_check('1117', :eg_version);
+
 INSERT INTO permission.perm_list (id, code, description) VALUES
  (608, 'APPLY_WORKSTATION_SETTING',
    oils_i18n_gettext(608, 'APPLY_WORKSTATION_SETTING', 'ppl', 'description'));

commit d423090e2277daa2e69f49cef91f37235184db95
Author: Bill Erickson <berickxx at gmail.com>
Date:   Fri Aug 3 11:39:15 2018 -0400

    LP#1750894 Remove errant phantomjs-killing comma
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/services/hatch.js b/Open-ILS/web/js/ui/default/staff/services/hatch.js
index f99fa34..12b144d 100644
--- a/Open-ILS/web/js/ui/default/staff/services/hatch.js
+++ b/Open-ILS/web/js/ui/default/staff/services/hatch.js
@@ -570,7 +570,7 @@ angular.module('egCoreMod')
 
                 checkOne(settings[0]);
                 return deferred.promise;
-            },
+            }
         );
     }
 

commit 73b7ace22996370f5bc8e338f115a3d9dd03b349
Author: Bill Erickson <berickxx at gmail.com>
Date:   Tue Jul 31 16:52:18 2018 -0400

    LP#1750894 Additional workstation setting types + repairs
    
    Added new workstation setting types:
    
    circ.checkout.strict_barcode
    cat.holdings_show_empty_org
    cat.copy.defaults
    cat.printlabels.default_template
    cat.printlabels.templates
    
    Bump perm ID in seed data, take 2.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
index 2aba1ba..c2eb7f6 100644
--- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql
+++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
@@ -1906,8 +1906,8 @@ INSERT INTO permission.perm_list ( id, code, description ) VALUES
     'Delete copy alerts', 'ppl', 'description' )),
  ( 607, 'EMERGENCY_CLOSING', oils_i18n_gettext( 607,
     'Create and manage Emergency Closings', 'ppl', 'description' )),
- (607, 'APPLY_WORKSTATION_SETTING',
-   oils_i18n_gettext(607, 'APPLY_WORKSTATION_SETTING', 'ppl', 'description'))
+ (608, 'APPLY_WORKSTATION_SETTING',
+   oils_i18n_gettext(608, 'APPLY_WORKSTATION_SETTING', 'ppl', 'description'))
 ;
 
 
@@ -18426,6 +18426,13 @@ VALUES (
         'cwst', 'label'
     )
 ), (
+    'circ.checkout.strict_barcode', 'circ', 'bool',
+    oils_i18n_gettext(
+        'circ.checkout.strict_barcode',
+        'Checkout: Strict Barcode',
+        'cwst', 'label'
+    )
+), (
     'cat.holdings_show_copies', 'cat', 'bool',
     oils_i18n_gettext(
         'cat.holdings_show_copies',
@@ -18440,6 +18447,13 @@ VALUES (
         'cwst', 'label'
     )
 ), (
+    'cat.holdings_show_empty_org', 'cat', 'bool',
+    oils_i18n_gettext(
+        'cat.holdings_show_empty_org',
+        'Holdings View Show Empty Orgs',
+        'cwst', 'label'
+    )
+), (
     'cat.holdings_show_vols', 'cat', 'bool',
     oils_i18n_gettext(
         'cat.holdings_show_vols',
@@ -18447,6 +18461,27 @@ VALUES (
         'cwst', 'label'
     )
 ), (
+    'cat.copy.defaults', 'cat', 'object',
+    oils_i18n_gettext(
+        'cat.copy.defaults',
+        'Copy Edit Default Values',
+        'cwst', 'label'
+    )
+), (
+    'cat.printlabels.default_template', 'cat', 'string',
+    oils_i18n_gettext(
+        'cat.printlabels.default_template',
+        'Print Label Default Template',
+        'cwst', 'label'
+    )
+), (
+    'cat.printlabels.templates', 'cat', 'object',
+    oils_i18n_gettext(
+        'cat.printlabels.templates',
+        'Print Label Templates',
+        'cwst', 'label'
+    )
+), (
     'eg.circ.patron.search.include_inactive', 'circ', 'bool',
     oils_i18n_gettext(
         'eg.circ.patron.search.include_inactive',
diff --git a/Open-ILS/src/sql/Pg/upgrade/YYYY.data.workstation-settings.sql b/Open-ILS/src/sql/Pg/upgrade/YYYY.data.workstation-settings.sql
index 12046cd..ba841cd 100644
--- a/Open-ILS/src/sql/Pg/upgrade/YYYY.data.workstation-settings.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/YYYY.data.workstation-settings.sql
@@ -97,6 +97,13 @@ VALUES (
         'cwst', 'label'
     )
 ), (
+    'circ.checkout.strict_barcode', 'circ', 'bool',
+    oils_i18n_gettext(
+        'circ.checkout.strict_barcode',
+        'Checkout: Strict Barcode',
+        'cwst', 'label'
+    )
+), (
     'cat.holdings_show_copies', 'cat', 'bool',
     oils_i18n_gettext(
         'cat.holdings_show_copies',
@@ -111,6 +118,13 @@ VALUES (
         'cwst', 'label'
     )
 ), (
+    'cat.holdings_show_empty_org', 'cat', 'bool',
+    oils_i18n_gettext(
+        'cat.holdings_show_empty_org',
+        'Holdings View Show Empty Orgs',
+        'cwst', 'label'
+    )
+), (
     'cat.holdings_show_vols', 'cat', 'bool',
     oils_i18n_gettext(
         'cat.holdings_show_vols',
@@ -118,6 +132,27 @@ VALUES (
         'cwst', 'label'
     )
 ), (
+    'cat.copy.defaults', 'cat', 'object',
+    oils_i18n_gettext(
+        'cat.copy.defaults',
+        'Copy Edit Default Values',
+        'cwst', 'label'
+    )
+), (
+    'cat.printlabels.default_template', 'cat', 'string',
+    oils_i18n_gettext(
+        'cat.printlabels.default_template',
+        'Print Label Default Template',
+        'cwst', 'label'
+    )
+), (
+    'cat.printlabels.templates', 'cat', 'object',
+    oils_i18n_gettext(
+        'cat.printlabels.templates',
+        'Print Label Templates',
+        'cwst', 'label'
+    )
+), (
     'eg.circ.patron.search.include_inactive', 'circ', 'bool',
     oils_i18n_gettext(
         'eg.circ.patron.search.include_inactive',

commit 29f35dcc1862db26f486e9e0c6a2c0174281019b
Author: Bill Erickson <berickxx at gmail.com>
Date:   Tue Jul 31 16:30:17 2018 -0400

    LP#1750894 Batch settings lookup handles migration
    
    As with a single getItem() call, the batch version of the call, when
    making server requests, will migrate any settings necessary from
    localStorage to server settings.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/services/hatch.js b/Open-ILS/web/js/ui/default/staff/services/hatch.js
index 55f13a2..f99fa34 100644
--- a/Open-ILS/web/js/ui/default/staff/services/hatch.js
+++ b/Open-ILS/web/js/ui/default/staff/services/hatch.js
@@ -250,6 +250,8 @@ angular.module('egCoreMod')
     // get the value for a stored item
     service.getItem = function(key) {
 
+        console.debug('getting item: ' + key);
+
         if (!service.keyStoredInBrowser(key)) {
             return service.getServerItem(key);
         }
@@ -544,19 +546,31 @@ angular.module('egCoreMod')
         var foundValues = {};
         return egNet.request(
             'open-ils.actor',
-            'open-ils.actor.settings.retrieve',
+            'open-ils.actor.settings.retrieve.atomic',
             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;
-            }
+            function(settings) { 
+                //return foundValues; 
+
+                var deferred = $q.defer();
+                function checkOne(setting) {
+                    if (!setting) {
+                        deferred.resolve(foundValues);
+                        return;
+                    }
+                    service.handleServerItemResponse(setting)
+                    .then(function(resp) {
+                        if (resp !== undefined) {
+                            foundValues[setting.name] = resp;
+                        }
+                        settings.shift();
+                        checkOne(settings[0]);
+                    });
+                }
+
+                checkOne(settings[0]);
+                return deferred.promise;
+            },
         );
     }
 

commit a61fc24476f6c24bceea2a4b0b9cc900ac0fe735
Author: Bill Erickson <berickxx at gmail.com>
Date:   Fri Jul 27 17:52:24 2018 -0400

    LP#1750894 Hatch set local storage item thinko
    
    Ensure hatch.setItem() returns a promise for both server- and browser-
    stored values.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/services/hatch.js b/Open-ILS/web/js/ui/default/staff/services/hatch.js
index 5642a93..55f13a2 100644
--- a/Open-ILS/web/js/ui/default/staff/services/hatch.js
+++ b/Open-ILS/web/js/ui/default/staff/services/hatch.js
@@ -373,7 +373,7 @@ angular.module('egCoreMod')
         }
 
         var deferred = $q.defer();
-        service.setBrowserItem(key, value).then(
+        return service.setBrowserItem(key, value).then(
             function(val) {deferred.resolve(val);},
 
             function() { // Hatch error

commit edd1e288917ddde1cebedf3df57732af5fc86d8a
Author: Bill Erickson <berickxx at gmail.com>
Date:   Fri Jul 27 17:26:08 2018 -0400

    LP#1750894 Bump perm ID; apply consistently
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
index b3f0a2c..2aba1ba 100644
--- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql
+++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
@@ -1905,9 +1905,12 @@ INSERT INTO permission.perm_list ( id, code, description ) VALUES
  ( 606, 'DELETE_COPY_ALERT', oils_i18n_gettext( 606,
     'Delete copy alerts', 'ppl', 'description' )),
  ( 607, 'EMERGENCY_CLOSING', oils_i18n_gettext( 607,
-    'Create and manage Emergency Closings', 'ppl', 'description' ))
+    'Create and manage Emergency Closings', 'ppl', 'description' )),
+ (607, 'APPLY_WORKSTATION_SETTING',
+   oils_i18n_gettext(607, 'APPLY_WORKSTATION_SETTING', 'ppl', 'description'))
 ;
 
+
 SELECT SETVAL('permission.perm_list_id_seq'::TEXT, 1000);
 
 INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm) VALUES
@@ -18330,10 +18333,6 @@ AND control_set = 1
 AND ahf.heading_purpose = 'related'
 AND ahf.heading_type = 'genre_form_term';
 
-INSERT INTO permission.perm_list (id, code, description) VALUES
- (607, 'APPLY_WORKSTATION_SETTING',
-   oils_i18n_gettext(607, 'APPLY_WORKSTATION_SETTING', 'ppl', 'description'));
-
 INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
 VALUES (
     'eg.circ.checkin.no_precat_alert', 'circ', 'bool',
diff --git a/Open-ILS/src/sql/Pg/upgrade/YYYY.data.workstation-settings.sql b/Open-ILS/src/sql/Pg/upgrade/YYYY.data.workstation-settings.sql
index d50c371..12046cd 100644
--- a/Open-ILS/src/sql/Pg/upgrade/YYYY.data.workstation-settings.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/YYYY.data.workstation-settings.sql
@@ -1,8 +1,8 @@
 BEGIN;
 
 INSERT INTO permission.perm_list (id, code, description) VALUES
- (607, 'APPLY_WORKSTATION_SETTING',
-   oils_i18n_gettext(607, 'APPLY_WORKSTATION_SETTING', 'ppl', 'description'));
+ (608, 'APPLY_WORKSTATION_SETTING',
+   oils_i18n_gettext(608, 'APPLY_WORKSTATION_SETTING', 'ppl', 'description'));
 
 INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
 VALUES (

commit 14803bb75f6129d59c3dc74e9c2df5909348969e
Author: Bill Erickson <berickxx at gmail.com>
Date:   Mon Jun 4 17:14:24 2018 -0400

    LP#1750894 Store grid limits in grid config
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/services/grid.js b/Open-ILS/web/js/ui/default/staff/services/grid.js
index b3fbda7..4c2ff19 100644
--- a/Open-ILS/web/js/ui/default/staff/services/grid.js
+++ b/Open-ILS/web/js/ui/default/staff/services/grid.js
@@ -171,6 +171,10 @@ angular.module('egGridMod',
 
                 var stored_limit = 0;
                 if ($scope.showPagination) {
+                    // localStorage of grid limits is deprecated. Limits 
+                    // are now stored along with the columns configuration.  
+                    // Values found in localStorage will be migrated upon 
+                    // config save.
                     if (grid.persistKey) {
                         var stored_limit = Number(
                             egCore.hatch.getLocalItem('eg.grid.' + grid.persistKey + '.limit')
@@ -324,9 +328,10 @@ angular.module('egGridMod',
                 }
 
                 controls.setLimit = function(limit,forget) {
-                    if (!forget && grid.persistKey)
-                        egCore.hatch.setLocalItem('eg.grid.' + grid.persistKey + '.limit', limit);
                     grid.limit = limit;
+                    if (!forget && grid.persistKey) {
+                        $scope.saveConfig();
+                    }
                 }
                 controls.getLimit = function() {
                     return grid.limit;
@@ -431,11 +436,11 @@ angular.module('egGridMod',
                 }
 
                 // only store information about visible columns.
-                var conf = grid.columnsProvider.columns.filter(
+                var cols = grid.columnsProvider.columns.filter(
                     function(col) {return Boolean(col.visible) });
 
                 // now scrunch the data down to just the needed info
-                conf = conf.map(function(col) {
+                cols = cols.map(function(col) {
                     var c = {name : col.name}
                     // Apart from the name, only store non-default values.
                     // No need to store col.visible, since that's implicit
@@ -445,12 +450,23 @@ angular.module('egGridMod',
                     return c;
                 });
 
+                var conf = {
+                    version: 2,
+                    limit: grid.limit,
+                    columns: cols
+                };
+
                 egCore.hatch.setItem('eg.grid.' + grid.persistKey, conf)
                 .then(function() { 
                     // Save operation performed from the grid configuration UI.
                     // Hide the configuration UI and re-draw w/ sort applied
                     if ($scope.showGridConf) 
                         $scope.toggleConfDisplay();
+
+                    // Once a version-2 grid config is saved (with limit
+                    // included) we can remove the local limit pref.
+                    egCore.hatch.removeLocalItem(
+                        'eg.grid.' + grid.persistKey + '.limit');
                 });
             }
 
@@ -468,7 +484,20 @@ angular.module('egGridMod',
                     var columns = grid.columnsProvider.columns;
                     var new_cols = [];
 
-                    angular.forEach(conf, function(col) {
+                    if (Array.isArray(conf)) {
+                        console.debug(  
+                            'upgrading version 1 grid config to version 2');
+                        conf = {
+                            version : 2,
+                            columns : conf
+                        };
+                    }
+
+                    if (conf.limit) {
+                        grid.limit = Number(conf.limit);
+                    }
+
+                    angular.forEach(conf.columns, function(col) {
                         var grid_col = columns.filter(
                             function(c) {return c.name == col.name})[0];
 
@@ -495,7 +524,7 @@ angular.module('egGridMod',
                     // configuration are marked as non-visible and 
                     // appended to the end of the new list of columns.
                     angular.forEach(columns, function(col) {
-                        var found = conf.filter(
+                        var found = conf.columns.filter(
                             function(c) {return (c.name == col.name)})[0];
                         if (!found) {
                             col.visible = false;
@@ -534,9 +563,10 @@ angular.module('egGridMod',
 
             $scope.limit = function(l) { 
                 if (angular.isNumber(l)) {
-                    if (grid.persistKey)
-                        egCore.hatch.setLocalItem('eg.grid.' + grid.persistKey + '.limit', l);
                     grid.limit = l;
+                    if (grid.persistKey) {
+                        $scope.saveConfig();
+                    }
                 }
                 return grid.limit 
             }

commit f06c3c2110e164b01a46014972258ba41423aa7a
Author: Bill Erickson <berickxx at gmail.com>
Date:   Mon Jun 4 12:10:20 2018 -0400

    LP#1750894 Server-stored workstaion prefs admin view
    
    Adds a new "Server Workstation Prefs" tab to the stored preferences
    workstation admin interface.  From here, users can view which
    preferences are stored as server-stored workstation preferences and
    delete select values.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor/Settings.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor/Settings.pm
index 29d5e9e..f627a16 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor/Settings.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor/Settings.pm
@@ -248,6 +248,14 @@ __PACKAGE__->register_method (
             This is a staff-only API created primarily to support the
             getKeys() functionality used in the browser client for
             server-managed settings.
+
+            Note as of now, this API can return names for user settings
+            which are unrelated to the staff client.  ALL user setting
+            names matching the selected prefix are returned!
+
+            Use the workstation_only option to avoid returning any user
+            setting names.
+
         /,
         params => [
             {desc => 'authtoken', type => 'string'},
@@ -264,12 +272,14 @@ __PACKAGE__->register_method (
 );
 
 sub applied_settings {
-    my ($self, $client, $auth, $prefix) = @_;
+    my ($self, $client, $auth, $prefix, $options) = @_;
 
     my $e = new_editor(authtoken => $auth);
     return $e->event unless $e->checkauth;
     return $e->event unless $e->allowed('STAFF_LOGIN');
 
+    $options ||= {};
+
     my $query = {
         select => {awss => ['name']},
         from => 'awss',
@@ -284,6 +294,8 @@ sub applied_settings {
         $client->respond($key->{name});
     }
 
+    return undef if $options->{workstation_only};
+
     $query = {
         select => {aus => ['name']},
         from => 'aus',
diff --git a/Open-ILS/src/templates/staff/admin/workstation/t_stored_prefs.tt2 b/Open-ILS/src/templates/staff/admin/workstation/t_stored_prefs.tt2
index dc031b4..641a73f 100644
--- a/Open-ILS/src/templates/staff/admin/workstation/t_stored_prefs.tt2
+++ b/Open-ILS/src/templates/staff/admin/workstation/t_stored_prefs.tt2
@@ -26,10 +26,13 @@ Click on the delete (X) button to remove a preference's value.
 
       <ul class="nav nav-tabs">
         <li ng-class="{active : context == 'local'}">
-          <a href='' ng-click="setContext('local')">[% l('Local Prefs') %]</a>
+          <a href='' ng-click="setContext('local')">[% l('In-Browser Prefs') %]</a>
         </li>
         <li ng-class="{active : context == 'remote'}">
-          <a href='' ng-click="setContext('remote')">[% l('Remote Prefs') %]</a>
+          <a href='' ng-click="setContext('remote')">[% l('Hatch Prefs') %]</a>
+        </li>
+        <li ng-class="{active : context == 'server_workstation'}">
+          <a href='' ng-click="setContext('server_workstation')">[% l('Server Workstation Prefs') %]</a>
         </li>
       </ul>
       <div class="tab-content">
diff --git a/Open-ILS/web/js/ui/default/staff/admin/workstation/app.js b/Open-ILS/web/js/ui/default/staff/admin/workstation/app.js
index 98dc9b7..9af1c00 100644
--- a/Open-ILS/web/js/ui/default/staff/admin/workstation/app.js
+++ b/Open-ILS/web/js/ui/default/staff/admin/workstation/app.js
@@ -746,7 +746,7 @@ function($scope , $q , egCore , egConfirmDialog) {
     // fetch the keys
 
     function refreshKeys() {
-        $scope.keys = {local : [], remote : []};
+        $scope.keys = {local : [], remote : [], server_workstation: []};
 
         if (egCore.hatch.hatchAvailable) {
             egCore.hatch.getRemoteKeys().then(
@@ -755,6 +755,9 @@ function($scope , $q , egCore , egConfirmDialog) {
     
         // local calls are non-async
         $scope.keys.local = egCore.hatch.getLocalKeys();
+
+        egCore.hatch.getServerKeys(null, {workstation_only: true}).then(
+            function(keys) {$scope.keys.server_workstation = keys});
     }
     refreshKeys();
 
@@ -764,11 +767,15 @@ function($scope , $q , egCore , egConfirmDialog) {
 
         if ($scope.context == 'local') {
             $scope.currentKeyContent = egCore.hatch.getLocalItem(key);
-        } else {
+        } else if ($scope.context == 'remote') {
             egCore.hatch.getRemoteItem(key)
             .then(function(content) {
                 $scope.currentKeyContent = content
             });
+        } else if ($scope.context == 'server_workstation') {
+            egCore.hatch.getServerItem(key).then(function(content) {
+                $scope.currentKeyContent = content;
+            });
         }
     }
 
@@ -784,11 +791,14 @@ function($scope , $q , egCore , egConfirmDialog) {
                     if ($scope.context == 'local') {
                         egCore.hatch.removeLocalItem(key);
                         refreshKeys();
-                    } else {
+                    } else if ($scope.context == 'remote') {
                         // Honor requests to remove items from Hatch even
                         // when Hatch is configured for data storage.
                         egCore.hatch.removeRemoteItem(key)
                         .then(function() { refreshKeys() });
+                    } else if ($scope.context == 'server_workstation') {
+                        egCore.hatch.removeServerItem(key)
+                        .then(function() { refreshKeys() });
                     }
                 },
                 cancel : function() {} // user canceled, nothing to do
diff --git a/Open-ILS/web/js/ui/default/staff/services/hatch.js b/Open-ILS/web/js/ui/default/staff/services/hatch.js
index d35c3f1..5642a93 100644
--- a/Open-ILS/web/js/ui/default/staff/services/hatch.js
+++ b/Open-ILS/web/js/ui/default/staff/services/hatch.js
@@ -741,13 +741,13 @@ angular.module('egCoreMod')
         return $q.when(service.getLocalKeys(prefix));
     }
 
-    service.getServerKeys = function(prefix) {
+    service.getServerKeys = function(prefix, options) {
         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.auth.token(), prefix, options
         );
     }
 
diff --git a/docs/RELEASE_NOTES_NEXT/Client/workstation-server-settings.adoc b/docs/RELEASE_NOTES_NEXT/Client/workstation-server-settings.adoc
index 94855df..b81eeda 100644
--- a/docs/RELEASE_NOTES_NEXT/Client/workstation-server-settings.adoc
+++ b/docs/RELEASE_NOTES_NEXT/Client/workstation-server-settings.adoc
@@ -28,6 +28,14 @@ consistent for all users.
 A setting is read-only when an org unit setting type exists (regardless of 
 whether a value is applied) and no user or workstation setting type exists.
 
+Server-Stored Workstation Settings Workstation Admin View
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+There's a new "Server Workstation Prefs" tab to the stored preferences
+workstation admin interface.  From here, users can view which
+preferences are stored as server-stored workstation preferences and
+delete select values.
+
 Upgrade Notes
 +++++++++++++
 

commit ff6aa2ab53271740a0b2fd73859af785f7765f32
Author: Bill Erickson <berickxx at gmail.com>
Date:   Wed May 30 16:38:56 2018 -0400

    LP#1750894 Workstation/Cascade setting pgtap tests
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/sql/Pg/t/lp1750894-workststation-settings.pg b/Open-ILS/src/sql/Pg/t/lp1750894-workststation-settings.pg
new file mode 100644
index 0000000..5a3384f
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/t/lp1750894-workststation-settings.pg
@@ -0,0 +1,84 @@
+BEGIN;
+
+SELECT plan(5);
+
+INSERT INTO actor.workstation (name, owning_lib) values ('test', 1);
+
+INSERT INTO actor.workstation_setting (workstation, name, value) 
+VALUES (
+    (SELECT id FROM actor.workstation WHERE name = 'test'),
+    'eg.search.adv_pane', '"test-workstation"'
+);
+
+
+-- duplicate the setting type as an org setting to verify precedence
+INSERT INTO config.org_unit_setting_type (name, grp, datatype, label)
+VALUES (
+    'eg.search.adv_pane', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.search.adv_pane',
+        'Catalog Advanced Search Default Pane',
+        'cwst', 'label'
+    )
+);
+
+INSERT INTO actor.org_unit_setting (org_unit, name, value) 
+    VALUES (1, 'eg.search.adv_pane', '"test-org-unit"');
+
+SELECT is(
+    (   SELECT value::TEXT
+        FROM actor.get_cascade_setting('eg.search.adv_pane',
+            1, 1, (SELECT id FROM actor.workstation WHERE name = 'test'))),
+    '"test-workstation"',
+    'Workstation setting takes precedence over org setting'
+);
+
+DELETE FROM actor.workstation_setting WHERE name = 'eg.search.adv_pane';
+
+SELECT is(
+    (   SELECT value::TEXT
+        FROM actor.get_cascade_setting('eg.search.adv_pane',
+            1, 1, (SELECT id FROM actor.workstation WHERE name = 'test'))),
+    '"test-org-unit"',
+    'Org unit setting should now work'
+);
+
+PREPARE user_setting_insert AS
+    INSERT INTO config.usr_setting_type (name, grp, datatype, label)
+    VALUES (
+        'eg.search.adv_pane', 'gui', 'string',
+        oils_i18n_gettext(
+            'eg.search.adv_pane',
+            'Catalog Advanced Search Default Pane',
+            'cwst', 'label'
+        )
+    );
+
+SELECT throws_like(
+    'user_setting_insert',
+    '%Cannot be used as both a user setting and a workstation setting.', 
+    'User settings cannot also be workstation settings'
+);
+
+DELETE FROM config.workstation_setting_type WHERE name = 'eg.search.adv_pane';
+
+SELECT lives_ok(
+    'user_setting_insert',
+    'User settings can now be created'
+);
+
+INSERT INTO actor.usr_setting (usr, name, value) 
+    VALUES (1, 'eg.search.adv_pane', '"test-user"');
+
+SELECT is(
+    (   SELECT value::TEXT
+        FROM actor.get_cascade_setting('eg.search.adv_pane',
+            1, 1, (SELECT id FROM actor.workstation WHERE name = 'test'))),
+    '"test-user"',
+    'User setting takes precedence over org setting'
+);
+
+
+-- Finish the tests and clean up.
+SELECT * FROM finish();
+ROLLBACK;

commit 7790968d6410a3de13ed606150b61f148f96b2bc
Author: Bill Erickson <berickxx at gmail.com>
Date:   Tue May 29 11:47:16 2018 -0400

    LP#1750894 Workstation/Cascade settings Release Notes
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/docs/RELEASE_NOTES_NEXT/Client/workstation-server-settings.adoc b/docs/RELEASE_NOTES_NEXT/Client/workstation-server-settings.adoc
new file mode 100644
index 0000000..94855df
--- /dev/null
+++ b/docs/RELEASE_NOTES_NEXT/Client/workstation-server-settings.adoc
@@ -0,0 +1,63 @@
+Browser Client Settings & Preferences Stored on the Server
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Browser cilent settings and preferences that should persist over time are
+now stored as settings on the server.  This allows settings to follow
+users and workstations and reduces problems associated with losing settings 
+as a result of clearing browser data.
+
+The browser client honors setting values stored as user settings, workstation
+settings, and org unit settings, depending on which setting types are
+locally configured.
+
+Setting Types
++++++++++++++
+
+* No setting can be both a user and workstation setting.  They are mutually
+  exclusive.
+* Any setting can be an org unit setting in addition to being a user or
+  workstaion setting.
+
+Read-Only Settings
+++++++++++++++++++
+
+Read-only settings are useful for defining values that staff can use but
+not modify.  For example, admins may wish to prevent users from locally
+modifying the grid configuration for a given interface so it remains
+consistent for all users.
+
+A setting is read-only when an org unit setting type exists (regardless of 
+whether a value is applied) and no user or workstation setting type exists.
+
+Upgrade Notes
++++++++++++++
+
+A new permission APPLY_WORKSTATION_SETTING has been added to control who
+may apply values to workstation settings.  Use something like the following
+to apply the permission to all staff accounts (mileage may vary):
+
+[source,sh]
+--------------------------------------------------------------------------
+INSERT INTO permission.grp_perm_map (grp, perm, depth) 
+VALUES (
+    (SELECT id FROM permission.grp_tree WHERE name = 'Staff'), -- name may vary
+    (SELECT id FROM permission.perm_list WHERE code = 'APPLY_WORKSTATION_SETTING'),
+    0 -- or 1, 2, etc.
+);
+--------------------------------------------------------------------------
+
+Workstation setting types matching values previously stored in the browser
+(via localStorage or Hatch) are created as part of this feature.  During
+upgrade, admins should consider whether any of these new setting types 
+should be transferred to user and/or org unit settings instead.  Setting
+type changes can be made at any time, but when a setting type is deleted
+all of its data is deleted, so a change in type means re-applying the 
+settings in the browser client.
+
+Values stored in the browser will automatically migrate to server settings
+as each setting is accessed in the browser client.  Once migrated, the
+in-browser copies are deleted.  
+
+If a setting type does not exist where the browser expects one, the 
+value is stored in-browser instead and a warning is issued in the console.
+
+

commit 7891c0241e9b5273f122f8d77cb20ebbf2d5cf6b
Author: Bill Erickson <berickxx at gmail.com>
Date:   Tue Mar 13 14:06:23 2018 -0400

    LP#1750894 Workstation & Cascade settings
    
    Adds a new config.workstation_setting_type table for managing
    workstation-specific settings.
    
    Adds new PG and perl API functionality for determining values for
    settings which may be represnted as workstation, user, and/or org unit
    settings.
    
    Teaches the AngularJS browser client to load and apply most settings at
    the server.  Values for settings stored in localStorage/Hatch are migrated
    to server settings at time of next use.
    
    Stock workstation setting types added to accommodate most browser client
    settings.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml
index 5d7be8c..66d146e 100644
--- a/Open-ILS/examples/fm_IDL.xml
+++ b/Open-ILS/examples/fm_IDL.xml
@@ -12499,8 +12499,52 @@ SELECT  usr,
 	</permacrud>
         </class>
 
-	<!-- ********************************************************************************************************************* -->
+	<class id="cwst" controller="open-ils.cstore open-ils.pcrud"
+		oils_obj:fieldmapper="config::workstation_setting_type" 
+		oils_persist:tablename="config.workstation_setting_type" 
+		reporter:label="Workstation Setting Type">
+		<fields oils_persist:primary="name">
+			<field name="name" reporter:datatype="text"/>
+			<field name="label" reporter:datatype="text" oils_persist:i18n="true"/>
+			<field name="description" reporter:datatype="text" oils_persist:i18n="true"/>
+			<field name="datatype" reporter:datatype="text"/>
+			<field name="fm_class" reporter:datatype="text"/>
+			<field name="grp" reporter:datatype="link"/>
+		</fields>
+		<links>
+			<link field="name" reltype="has_many" key="name" map="" class="aous"/>
+			<link field="grp" reltype="has_a" key="name" map="" class="csg"/>
+		</links>
+		<permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+			<actions>
+				<create permission="ADMIN_WORKSTATION_SETTING_TYPE" global_required="true"/>
+				<retrieve/>
+				<update permission="ADMIN_WORKSTATION_SETTING_TYPE" global_required="true"/>
+				<delete permission="ADMIN_WORKSTATION_SETTING_TYPE" global_required="true"/>
+			</actions>
+		</permacrud>
+	</class>
 
+	<!-- no pcrud access is granted for now, because it's assumed these
+			 setting values will be applied and retrived via the API. -->
+	<class id="awss" 
+		controller="open-ils.cstore"
+		oils_obj:fieldmapper="actor::workstation_setting" 
+		oils_persist:tablename="actor.workstation_setting" 
+		reporter:label="Workstation Setting">
+		<fields oils_persist:primary="id" oils_persist:sequence="actor.workstation_setting_id_seq">
+			<field reporter:label="Setting ID" name="id" reporter:datatype="id" />
+			<field reporter:label="Name" name="name" reporter:datatype="link"/>
+			<field reporter:label="Value" name="value" reporter:datatype="text"/>
+			<field reporter:label="Workstation" name="workstation" reporter:datatype="link"/>
+		</fields>
+		<links>
+			<link field="name" reltype="has_a" key="name" map="" class="cwst"/>
+			<link field="workstation" reltype="has_a" key="id" map="" class="aws"/>
+		</links>
+	</class>
+
+	<!-- ********************************************************************************************************************* -->
 </IDL>
 
 <!--
diff --git a/Open-ILS/src/perlmods/MANIFEST b/Open-ILS/src/perlmods/MANIFEST
index cb980a6..8473b19 100644
--- a/Open-ILS/src/perlmods/MANIFEST
+++ b/Open-ILS/src/perlmods/MANIFEST
@@ -18,6 +18,7 @@ lib/OpenILS/Application/Actor/Container.pm
 lib/OpenILS/Application/Actor/Friends.pm
 lib/OpenILS/Application/Actor/Stage.pm
 lib/OpenILS/Application/Actor/UserGroups.pm
+lib/OpenILS/Application/Actor/Settings.pm
 lib/OpenILS/Application/AppUtils.pm
 lib/OpenILS/Application/Booking.pm
 lib/OpenILS/Application/Cat.pm
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm
index e62982e..503cf35 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm
@@ -31,6 +31,7 @@ use OpenILS::Application::Actor::ClosedDates;
 use OpenILS::Application::Actor::UserGroups;
 use OpenILS::Application::Actor::Friends;
 use OpenILS::Application::Actor::Stage;
+use OpenILS::Application::Actor::Settings;
 
 use OpenILS::Utils::CStoreEditor qw/:funcs/;
 use OpenILS::Utils::Penalty;
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor/Settings.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor/Settings.pm
new file mode 100644
index 0000000..29d5e9e
--- /dev/null
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor/Settings.pm
@@ -0,0 +1,306 @@
+package OpenILS::Application::Actor::Settings;
+use strict; use warnings;
+use base 'OpenILS::Application';
+use OpenSRF::AppSession;
+use OpenSRF::Utils::Logger q/$logger/;
+use OpenILS::Application::AppUtils;
+use OpenILS::Utils::CStoreEditor q/:funcs/;
+use OpenILS::Utils::Fieldmapper;
+use OpenSRF::Utils::JSON;
+use OpenILS::Event;
+my $U = "OpenILS::Application::AppUtils";
+
+# Setting names may only contains letters, numbers, unders, and dots.
+my $name_regex = qr/[^a-zA-Z0-9_\.]/;
+
+__PACKAGE__->register_method (
+    method      => 'retrieve_settings',
+    api_name    => 'open-ils.actor.settings.retrieve',
+    stream      => 1,
+    signature => {
+        desc => q/
+            Returns org unit, user, and workstation setting values
+            for the requested setting types.
+
+            The API makes a best effort to find the correct setting
+            value based on the available context data.
+
+            If no auth token is provided, only publicly visible org
+            unit settings may be returned.
+
+            If no workstation is linked to the provided auth token, only
+            user settings and perm-visible org unit settings may be
+            returned.
+
+            If no org unit is provided, but a workstation is linked to the
+            auth token, the owning lib of the workstation is used as the
+            context org unit.
+        /,
+        params => [
+            {desc => 'settings. List of setting names', type => 'array'},
+            {desc => 'authtoken. Optional', type => 'string'},
+            {desc => 'org_id. Optional', type => 'number'}
+        ],
+        return => {
+            desc => q/
+                Stream of setting name=>value pairs in the same order
+                as the provided list of setting names.  No key-value
+                pair is returned for settings that have no value defined./,
+            type => 'string'
+        }
+    }
+);
+
+sub retrieve_settings {
+    my ($self, $client, $settings, $auth, $org_id) = @_;
+
+    my ($aou_id, $user_id, $ws_id, $evt) = get_context($auth, $org_id);
+    return $evt if $evt; # bogus auth token
+
+    return OpenILS::Event->new('BAD_PARAMS',
+        desc => 'Cannot retrieve settings without a user or org unit')
+        unless ($user_id || $aou_id);
+
+    # Setting names may only contains letters, numbers, unders, and dots.
+    s/$name_regex//g foreach @$settings;
+
+    # Encode as a db-friendly array.
+    my $settings_str = '{' . join(',', @$settings) . '}';
+
+    # Some settings could be bulky, so fetch them as a stream from
+    # cstore, relaying values back to the caller as they arrive.
+    my $ses = OpenSRF::AppSession->create('open-ils.cstore');
+    my $req = $ses->request('open-ils.cstore.json_query', {
+        from => [
+            'actor.get_cascade_setting_batch',
+            $settings_str, $aou_id, $user_id, $ws_id
+        ]
+    });
+
+    while (my $resp = $req->recv) {
+        my $summary = $resp->content;
+        $summary->{value} = OpenSRF::Utils::JSON->JSON2perl($summary->{value});
+        $client->respond($summary);
+    }
+
+    $ses->kill_me;
+    return undef;
+}
+
+# Returns ($org_id, $user_id, $ws_id, $evt);
+# Any value may be undef.
+sub get_context {
+    my ($auth, $org_id) = @_;
+
+    return ($org_id) unless $auth;
+
+    my $e = new_editor(authtoken => $auth);
+    return (undef, undef, undef, $e->event) unless $e->checkauth;
+
+    my $user_id = $e->requestor->id;
+    my $ws_id = $e->requestor->wsid;
+
+    # default to the workstation org if needed.
+    $org_id = $e->requestor->ws_ou if $ws_id && !$org_id;
+
+    return ($org_id, $user_id, $ws_id);
+}
+
+__PACKAGE__->register_method (
+    method      => 'apply_user_or_ws_setting',
+    api_name    => 'open-ils.actor.settings.apply.user_or_ws',
+    stream      => 1,
+    signature => {
+        desc => q/
+            Apply values to user or workstation settings, depending
+            on which is supported via local configuration.
+
+            The API ignores nonexistent settings and only returns error
+            events when an auth, permission, or internal error occurs.
+        /,
+        params => [
+            {desc => 'authtoken', type => 'string'},
+            {desc => 'settings. Hash of key/value pairs', type => 'object'},
+        ],
+        return => {
+            desc => 'Returns the number of applied settings on succes, Event on error.',
+            type => 'number or event'
+        }
+    }
+);
+
+sub apply_user_or_ws_setting {
+    my ($self, $client, $auth, $settings) = @_;
+
+    my $e = new_editor(authtoken => $auth, xact => 1);
+    return $e->die_event unless $e->checkauth;
+
+    my $applied = 0;
+    my $ws_allowed = 0;
+
+    for my $name (keys %$settings) {
+        $name =~ s/$name_regex//g;
+        my $val = $$settings{$name};
+        my $stype = $e->retrieve_config_usr_setting_type($name);
+
+        if ($stype) {
+            my $evt = apply_user_setting($e, $name, $val);
+            return $evt if $evt;
+            $applied++;
+
+        } elsif ($e->requestor->wsid) {
+            $stype = $e->retrieve_config_workstation_setting_type($name);
+            next unless $stype; # no such workstation setting, skip.
+
+            if (!$ws_allowed) {
+                # Confirm the caller has permission to apply workstation
+                # settings at the logged-in workstation before applying.
+                # Do the perm check here so it's only needed once per batch.
+                return $e->die_event unless
+                    $ws_allowed = $e->allowed('APPLY_WORKSTATION_SETTING');
+            }
+
+            my $evt = apply_workstation_setting($e, $name, $val);
+            return $evt if $evt;
+            $applied++;
+        }
+    }
+
+    $e->commit if $applied > 0;
+    $e->rollback if $applied == 0;
+
+    return $applied;
+}
+
+# CUD for user settings.
+# Returns undef on success, Event on error.
+# NOTE: This code was copied as-is from
+# open-ils.actor.patron.settings.update, because it lets us
+# manage the batch of updates within a single transaction.  Also
+# worth noting the APIs in this mod could eventually replace
+# open-ils.actor.patron.settings.update.  Maybe.
+sub apply_user_setting {
+    my ($e, $name, $val) = @_;
+    my $user_id = $e->requestor->id;
+
+    my $set = $e->search_actor_user_setting(
+        {usr => $user_id, name => $name})->[0];
+
+    if (defined $val) {
+        $val = OpenSRF::Utils::JSON->perl2JSON($val);
+        if ($set) {
+            $set->value($val);
+            $e->update_actor_user_setting($set) or return $e->die_event;
+        } else {
+            $set = Fieldmapper::actor::user_setting->new;
+            $set->usr($user_id);
+            $set->name($name);
+            $set->value($val);
+            $e->create_actor_user_setting($set) or return $e->die_event;
+        }
+    } elsif ($set) {
+        $e->delete_actor_user_setting($set) or return $e->die_event;
+    }
+
+    return undef;
+}
+
+# CUD for workstation settings.
+# Assumes ->wsid contains a value and permissions have been checked.
+# Returns undef on success, Event on error.
+sub apply_workstation_setting {
+    my ($e, $name, $val) = @_;
+    my $ws_id = $e->requestor->wsid;
+
+    my $set = $e->search_actor_workstation_setting(
+        {workstation => $ws_id, name => $name})->[0];
+
+    if (defined $val) {
+        $val = OpenSRF::Utils::JSON->perl2JSON($val);
+
+        if ($set) {
+            $set->value($val);
+            $e->update_actor_workstation_setting($set) or return $e->die_event;
+        } else {
+            $set = Fieldmapper::actor::workstation_setting->new;
+            $set->workstation($ws_id);
+            $set->name($name);
+            $set->value($val);
+            $e->create_actor_workstation_setting($set) or return $e->die_event;
+        }
+    } elsif ($set) {
+        $e->delete_actor_workstation_setting($set) or return $e->die_event;
+    }
+
+    return undef;
+}
+
+__PACKAGE__->register_method (
+    method      => 'applied_settings',
+    api_name    => 'open-ils.actor.settings.staff.applied.names',
+    stream      => 1,
+    authoritative => 1,
+    signature => {
+        desc => q/
+            Returns a list of setting names where a value is applied to
+            the current user or workstation.
+
+            This is a staff-only API created primarily to support the
+            getKeys() functionality used in the browser client for
+            server-managed settings.
+        /,
+        params => [
+            {desc => 'authtoken', type => 'string'},
+            {desc =>
+                'prefix.  Limit keys to those starting with $prefix',
+             type => 'string'
+            },
+        ],
+        return => {
+            desc => 'List of strings, Event on error',
+            type => 'array'
+        }
+    }
+);
+
+sub applied_settings {
+    my ($self, $client, $auth, $prefix) = @_;
+
+    my $e = new_editor(authtoken => $auth);
+    return $e->event unless $e->checkauth;
+    return $e->event unless $e->allowed('STAFF_LOGIN');
+
+    my $query = {
+        select => {awss => ['name']},
+        from => 'awss',
+        where => {
+            workstation => $e->requestor->wsid
+        }
+    };
+
+    $query->{where}->{name} = {like => "$prefix%"} if $prefix;
+
+    for my $key (@{$e->json_query($query)}) {
+        $client->respond($key->{name});
+    }
+
+    $query = {
+        select => {aus => ['name']},
+        from => 'aus',
+        where => {
+            usr => $e->requestor->id
+        }
+    };
+
+    $query->{where}->{name} = {like => "$prefix%"} if $prefix;
+
+    for my $key (@{$e->json_query($query)}) {
+        $client->respond($key->{name});
+    }
+
+    return undef;
+}
+
+
+
+1;
diff --git a/Open-ILS/src/sql/Pg/002.schema.config.sql b/Open-ILS/src/sql/Pg/002.schema.config.sql
index b44ebbd..31cd398 100644
--- a/Open-ILS/src/sql/Pg/002.schema.config.sql
+++ b/Open-ILS/src/sql/Pg/002.schema.config.sql
@@ -683,6 +683,64 @@ CREATE TABLE config.usr_setting_type (
 
 );
 
+CREATE TABLE config.workstation_setting_type (
+    name            TEXT    PRIMARY KEY,
+    label           TEXT    UNIQUE NOT NULL,
+    grp             TEXT    REFERENCES config.settings_group (name),
+    description     TEXT,
+    datatype        TEXT    NOT NULL DEFAULT 'string',
+    fm_class        TEXT,
+    --
+    -- define valid datatypes
+    --
+    CONSTRAINT cwst_valid_datatype CHECK ( datatype IN
+    ( 'bool', 'integer', 'float', 'currency', 'interval',
+      'date', 'string', 'object', 'array', 'link' ) ),
+    --
+    -- fm_class is meaningful only for 'link' datatype
+    --
+    CONSTRAINT cwst_no_empty_link CHECK
+    ( ( datatype =  'link' AND fm_class IS NOT NULL ) OR
+      ( datatype <> 'link' AND fm_class IS NULL ) )
+);
+
+-- Prevent setting types from being both user and workstation settings.
+CREATE OR REPLACE FUNCTION config.setting_is_user_or_ws()
+RETURNS TRIGGER AS $FUNC$
+BEGIN
+
+    IF TG_TABLE_NAME = 'usr_setting_type' THEN
+        PERFORM TRUE FROM config.workstation_setting_type cwst
+            WHERE cwst.name = NEW.name;
+        IF NOT FOUND THEN
+            RETURN NULL;
+        END IF;
+    END IF;
+
+    IF TG_TABLE_NAME = 'workstation_setting_type' THEN
+        PERFORM TRUE FROM config.usr_setting_type cust
+            WHERE cust.name = NEW.name;
+        IF NOT FOUND THEN
+            RETURN NULL;
+        END IF;
+    END IF;
+
+    RAISE EXCEPTION 
+        '% Cannot be used as both a user setting and a workstation setting.', 
+        NEW.name;
+END;
+$FUNC$ LANGUAGE PLPGSQL STABLE;
+
+CREATE CONSTRAINT TRIGGER check_setting_is_usr_or_ws
+  AFTER INSERT OR UPDATE ON config.usr_setting_type
+  FOR EACH ROW EXECUTE PROCEDURE config.setting_is_user_or_ws();
+
+CREATE CONSTRAINT TRIGGER check_setting_is_usr_or_ws
+  AFTER INSERT OR UPDATE ON config.workstation_setting_type
+  FOR EACH ROW EXECUTE PROCEDURE config.setting_is_user_or_ws();
+
+
+
 -- Some handy functions, based on existing ones, to provide optional ingest normalization
 
 CREATE OR REPLACE FUNCTION public.left_trunc( TEXT, INT ) RETURNS TEXT AS $func$
diff --git a/Open-ILS/src/sql/Pg/005.schema.actors.sql b/Open-ILS/src/sql/Pg/005.schema.actors.sql
index d96f83d..1152d97 100644
--- a/Open-ILS/src/sql/Pg/005.schema.actors.sql
+++ b/Open-ILS/src/sql/Pg/005.schema.actors.sql
@@ -1054,4 +1054,168 @@ BEGIN
     END LOOP;
 END $$ LANGUAGE PLPGSQL;
 
+CREATE TABLE actor.workstation_setting (
+    id          SERIAL PRIMARY KEY,
+    workstation INT    NOT NULL REFERENCES actor.workstation (id) 
+                       ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    name        TEXT   NOT NULL REFERENCES config.workstation_setting_type (name) 
+                       ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    value       JSON   NOT NULL
+);
+
+CREATE INDEX actor_workstation_setting_workstation_idx 
+    ON actor.workstation_setting (workstation);
+
+CREATE TYPE actor.cascade_setting_summary AS (
+    name TEXT,
+    value JSON,
+    has_org_setting BOOLEAN,
+    has_user_setting BOOLEAN,
+    has_workstation_setting BOOLEAN
+);
+
+CREATE OR REPLACE FUNCTION actor.get_cascade_setting(
+    setting_name TEXT, org_id INT, user_id INT, workstation_id INT) 
+    RETURNS actor.cascade_setting_summary AS
+$FUNC$
+DECLARE
+    setting_value JSON;
+    summary actor.cascade_setting_summary;
+    org_setting_type config.org_unit_setting_type%ROWTYPE;
+BEGIN
+
+    summary.name := setting_name;
+
+    -- Collect the org setting type status first in case we exit early.
+    -- The existance of an org setting type is not considered
+    -- privileged information.
+    SELECT INTO org_setting_type * 
+        FROM config.org_unit_setting_type WHERE name = setting_name;
+    IF FOUND THEN
+        summary.has_org_setting := TRUE;
+    ELSE
+        summary.has_org_setting := FALSE;
+    END IF;
+
+    -- User and workstation settings have the same priority.
+    -- Start with user settings since that's the simplest code path.
+    -- The workstation_id is ignored if no user_id is provided.
+    IF user_id IS NOT NULL THEN
+
+        SELECT INTO summary.value value FROM actor.usr_setting
+            WHERE usr = user_id AND name = setting_name;
+
+        IF FOUND THEN
+            -- if we have a value, we have a setting type
+            summary.has_user_setting := TRUE;
+
+            IF workstation_id IS NOT NULL THEN
+                -- Only inform the caller about the workstation
+                -- setting type disposition when a workstation id is
+                -- provided.  Otherwise, it's NULL to indicate UNKNOWN.
+                summary.has_workstation_setting := FALSE;
+            END IF;
+
+            RETURN summary;
+        END IF;
+
+        -- no user setting value, but a setting type may exist
+        SELECT INTO summary.has_user_setting EXISTS (
+            SELECT TRUE FROM config.usr_setting_type 
+            WHERE name = setting_name
+        );
+
+        IF workstation_id IS NOT NULL THEN 
+
+            IF NOT summary.has_user_setting THEN
+                -- A workstation setting type may only exist when a user
+                -- setting type does not.
+
+                SELECT INTO summary.value value 
+                    FROM actor.workstation_setting         
+                    WHERE workstation = workstation_id AND name = setting_name;
+
+                IF FOUND THEN
+                    -- if we have a value, we have a setting type
+                    summary.has_workstation_setting := TRUE;
+                    RETURN summary;
+                END IF;
+
+                -- no value, but a setting type may exist
+                SELECT INTO summary.has_workstation_setting EXISTS (
+                    SELECT TRUE FROM config.workstation_setting_type 
+                    WHERE name = setting_name
+                );
+            END IF;
+
+            -- Finally make use of the workstation to determine the org
+            -- unit if none is provided.
+            IF org_id IS NULL AND summary.has_org_setting THEN
+                SELECT INTO org_id owning_lib 
+                    FROM actor.workstation WHERE id = workstation_id;
+            END IF;
+        END IF;
+    END IF;
+
+    -- Some org unit settings are protected by a view permission.
+    -- First see if we have any data that needs protecting, then 
+    -- check the permission if needed.
+
+    IF NOT summary.has_org_setting THEN
+        RETURN summary;
+    END IF;
+
+    -- avoid putting the value into the summary until we confirm
+    -- the value should be visible to the caller.
+    SELECT INTO setting_value value 
+        FROM actor.org_unit_ancestor_setting(setting_name, org_id);
+
+    IF NOT FOUND THEN
+        -- No value found -- perm check is irrelevant.
+        RETURN summary;
+    END IF;
+
+    IF org_setting_type.view_perm IS NOT NULL THEN
+
+        IF user_id IS NULL THEN
+            RAISE NOTICE 'Perm check required but no user_id provided';
+            RETURN summary;
+        END IF;
+
+        IF NOT permission.usr_has_perm(
+            user_id, (SELECT code FROM permission.perm_list 
+                WHERE id = org_setting_type.view_perm), org_id) 
+        THEN
+            RAISE NOTICE 'Perm check failed for user % on %',
+                user_id, org_setting_type.view_perm;
+            RETURN summary;
+        END IF;
+    END IF;
+
+    -- Perm check succeeded or was not necessary.
+    summary.value := setting_value;
+    RETURN summary;
+END;
+$FUNC$ LANGUAGE PLPGSQL;
+
+
+CREATE OR REPLACE FUNCTION actor.get_cascade_setting_batch(
+    setting_names TEXT[], org_id INT, user_id INT, workstation_id INT) 
+    RETURNS SETOF actor.cascade_setting_summary AS
+$FUNC$
+-- Returns a row per setting matching the setting name order.  If no 
+-- value is applied, NULL is returned to retain name-response ordering.
+DECLARE
+    setting_name TEXT;
+    summary actor.cascade_setting_summary;
+BEGIN
+    FOREACH setting_name IN ARRAY setting_names LOOP
+        SELECT INTO summary * FROM actor.get_cascade_setting(
+            setting_Name, org_id, user_id, workstation_id);
+        RETURN NEXT summary;
+    END LOOP;
+END;
+$FUNC$ LANGUAGE PLPGSQL;
+
+
 COMMIT;
diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
index 4213a0c..b3f0a2c 100644
--- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql
+++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
@@ -2094,7 +2094,9 @@ INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
 			'VIEW_PERMIT_CHECKOUT',
 			'VIEW_USER',
 			'VIEW_USER_FINES_SUMMARY',
-			'VIEW_USER_TRANSACTIONS');
+			'VIEW_USER_TRANSACTIONS',
+            'APPLY_WORKSTATION_SETTING'
+        );
 
 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
 	SELECT
@@ -18327,3 +18329,893 @@ WHERE tag = '555'
 AND control_set = 1
 AND ahf.heading_purpose = 'related'
 AND ahf.heading_type = 'genre_form_term';
+
+INSERT INTO permission.perm_list (id, code, description) VALUES
+ (607, 'APPLY_WORKSTATION_SETTING',
+   oils_i18n_gettext(607, 'APPLY_WORKSTATION_SETTING', 'ppl', 'description'));
+
+INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
+VALUES (
+    'eg.circ.checkin.no_precat_alert', 'circ', 'bool',
+    oils_i18n_gettext(
+        'eg.circ.checkin.no_precat_alert',
+        'Checkin: Ignore Precataloged Items',
+        'cwst', 'label'
+    )
+), (
+    'eg.circ.checkin.noop', 'circ', 'bool',
+    oils_i18n_gettext(
+        'eg.circ.checkin.noop',
+        'Checkin: Suppress Holds and Transits',
+        'cwst', 'label'
+    )
+), (
+    'eg.circ.checkin.void_overdues', 'circ', 'bool',
+    oils_i18n_gettext(
+        'eg.circ.checkin.void_overdues',
+        'Checkin: Amnesty Mode',
+        'cwst', 'label'
+    )
+), (
+    'eg.circ.checkin.auto_print_holds_transits', 'circ', 'bool',
+    oils_i18n_gettext(
+        'eg.circ.checkin.auto_print_holds_transits',
+        'Checkin: Auto-Print Holds and Transits',
+        'cwst', 'label'
+    )
+), (
+    'eg.circ.checkin.clear_expired', 'circ', 'bool',
+    oils_i18n_gettext(
+        'eg.circ.checkin.clear_expired',
+        'Checkin: Clear Holds Shelf',
+        'cwst', 'label'
+    )
+), (
+    'eg.circ.checkin.retarget_holds', 'circ', 'bool',
+    oils_i18n_gettext(
+        'eg.circ.checkin.retarget_holds',
+        'Checkin: Retarget Local Holds',
+        'cwst', 'label'
+    )
+), (
+    'eg.circ.checkin.retarget_holds_all', 'circ', 'bool',
+    oils_i18n_gettext(
+        'eg.circ.checkin.retarget_holds_all',
+        'Checkin: Retarget All Statuses',
+        'cwst', 'label'
+    )
+), (
+    'eg.circ.checkin.hold_as_transit', 'circ', 'bool',
+    oils_i18n_gettext(
+        'eg.circ.checkin.hold_as_transit',
+        'Checkin: Capture Local Holds as Transits',
+        'cwst', 'label'
+    )
+), (
+    'eg.circ.checkin.manual_float', 'circ', 'bool',
+    oils_i18n_gettext(
+        'eg.circ.checkin.manual_float',
+        'Checkin: Manual Floating Active',
+        'cwst', 'label'
+    )
+), (
+    'eg.circ.patron.summary.collapse', 'circ', 'bool',
+    oils_i18n_gettext(
+        'eg.circ.patron.summary.collapse',
+        'Collaps Patron Summary Display',
+        'cwst', 'label'
+    )
+), (
+    'circ.bills.receiptonpay', 'circ', 'bool',
+    oils_i18n_gettext(
+        'circ.bills.receiptonpay',
+        'Print Receipt On Payment',
+        'cwst', 'label'
+    )
+), (
+    'circ.renew.strict_barcode', 'circ', 'bool',
+    oils_i18n_gettext(
+        'circ.renew.strict_barcode',
+        'Renew: Strict Barcode',
+        'cwst', 'label'
+    )
+), (
+    'circ.checkin.strict_barcode', 'circ', 'bool',
+    oils_i18n_gettext(
+        'circ.checkin.strict_barcode',
+        'Checkin: Strict Barcode',
+        'cwst', 'label'
+    )
+), (
+    'cat.holdings_show_copies', 'cat', 'bool',
+    oils_i18n_gettext(
+        'cat.holdings_show_copies',
+        'Holdings View Show Copies',
+        'cwst', 'label'
+    )
+), (
+    'cat.holdings_show_empty', 'cat', 'bool',
+    oils_i18n_gettext(
+        'cat.holdings_show_empty',
+        'Holdings View Show Empty Volumes',
+        'cwst', 'label'
+    )
+), (
+    'cat.holdings_show_vols', 'cat', 'bool',
+    oils_i18n_gettext(
+        'cat.holdings_show_vols',
+        'Holdings View Show Volumes',
+        'cwst', 'label'
+    )
+), (
+    'eg.circ.patron.search.include_inactive', 'circ', 'bool',
+    oils_i18n_gettext(
+        'eg.circ.patron.search.include_inactive',
+        'Patron Search Include Inactive',
+        'cwst', 'label'
+    )
+), (
+    'eg.circ.patron.search.show_extras', 'circ', 'bool',
+    oils_i18n_gettext(
+        'eg.circ.patron.search.show_extras',
+        'Patron Search Show Extra Search Options',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.checkin.checkin', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.checkin.checkin',
+        'Grid Config: circ.checkin.checkin',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.checkin.capture', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.checkin.capture',
+        'Grid Config: circ.checkin.capture',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.server.config.copy_tag_type', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.admin.server.config.copy_tag_type',
+        'Grid Config: admin.server.config.copy_tag_type',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.server.config.metabib_field_virtual_map.grid', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.admin.server.config.metabib_field_virtual_map.grid',
+        'Grid Config: admin.server.config.metabib_field_virtual_map.grid',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.server.config.metabib_field.grid', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.admin.server.config.metabib_field.grid',
+        'Grid Config: admin.server.config.metabib_field.grid',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.server.config.marc_field', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.admin.server.config.marc_field',
+        'Grid Config: admin.server.config.marc_field',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.server.asset.copy_tag', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.admin.server.asset.copy_tag',
+        'Grid Config: admin.server.asset.copy_tag',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.local.circ.neg_balance_users', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.admin.local.circ.neg_balance_users',
+        'Grid Config: admin.local.circ.neg_balance_users',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.local.rating.badge', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.admin.local.rating.badge',
+        'Grid Config: admin.local.rating.badge',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.workstation.work_log', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.admin.workstation.work_log',
+        'Grid Config: admin.workstation.work_log',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.workstation.patron_log', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.admin.workstation.patron_log',
+        'Grid Config: admin.workstation.patron_log',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.serials.pattern_template', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.admin.serials.pattern_template',
+        'Grid Config: admin.serials.pattern_template',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.serials.copy_templates', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.serials.copy_templates',
+        'Grid Config: serials.copy_templates',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.cat.record_overlay.holdings', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.record_overlay.holdings',
+        'Grid Config: cat.record_overlay.holdings',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.cat.bucket.record.search', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.bucket.record.search',
+        'Grid Config: cat.bucket.record.search',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.cat.bucket.record.view', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.bucket.record.view',
+        'Grid Config: cat.bucket.record.view',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.cat.bucket.record.pending', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.bucket.record.pending',
+        'Grid Config: cat.bucket.record.pending',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.cat.bucket.copy.view', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.bucket.copy.view',
+        'Grid Config: cat.bucket.copy.view',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.cat.bucket.copy.pending', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.bucket.copy.pending',
+        'Grid Config: cat.bucket.copy.pending',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.cat.items', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.items',
+        'Grid Config: cat.items',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.cat.volcopy.copies', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.volcopy.copies',
+        'Grid Config: cat.volcopy.copies',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.cat.volcopy.copies.complete', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.volcopy.copies.complete',
+        'Grid Config: cat.volcopy.copies.complete',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.cat.peer_bibs', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.peer_bibs',
+        'Grid Config: cat.peer_bibs',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.cat.catalog.holds', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.catalog.holds',
+        'Grid Config: cat.catalog.holds',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.cat.holdings', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.holdings',
+        'Grid Config: cat.holdings',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.cat.z3950_results', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.z3950_results',
+        'Grid Config: cat.z3950_results',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.holds.shelf', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.holds.shelf',
+        'Grid Config: circ.holds.shelf',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.holds.pull', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.holds.pull',
+        'Grid Config: circ.holds.pull',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.in_house_use', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.in_house_use',
+        'Grid Config: circ.in_house_use',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.renew', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.renew',
+        'Grid Config: circ.renew',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.transits.list', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.transits.list',
+        'Grid Config: circ.transits.list',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.patron.holds', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.patron.holds',
+        'Grid Config: circ.patron.holds',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.pending_patrons.list', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.pending_patrons.list',
+        'Grid Config: circ.pending_patrons.list',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.patron.items_out.noncat', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.patron.items_out.noncat',
+        'Grid Config: circ.patron.items_out.noncat',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.patron.items_out', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.patron.items_out',
+        'Grid Config: circ.patron.items_out',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.patron.billhistory_payments', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.patron.billhistory_payments',
+        'Grid Config: circ.patron.billhistory_payments',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.user.bucket.view', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.user.bucket.view',
+        'Grid Config: user.bucket.view',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.user.bucket.pending', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.user.bucket.pending',
+        'Grid Config: user.bucket.pending',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.patron.staff_messages', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.patron.staff_messages',
+        'Grid Config: circ.patron.staff_messages',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.patron.archived_messages', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.patron.archived_messages',
+        'Grid Config: circ.patron.archived_messages',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.patron.bills', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.patron.bills',
+        'Grid Config: circ.patron.bills',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.patron.checkout', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.patron.checkout',
+        'Grid Config: circ.patron.checkout',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.serials.mfhd_grid', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.serials.mfhd_grid',
+        'Grid Config: serials.mfhd_grid',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.serials.view_item_grid', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.serials.view_item_grid',
+        'Grid Config: serials.view_item_grid',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.serials.dist_stream_grid', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.serials.dist_stream_grid',
+        'Grid Config: serials.dist_stream_grid',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.patron.search', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.patron.search',
+        'Grid Config: circ.patron.search',
+        'cwst', 'label'
+    )
+), (
+    'eg.cat.record.summary.collapse', 'gui', 'bool',
+    oils_i18n_gettext(
+        'eg.cat.record.summary.collapse',
+        'Collapse Bib Record Summary',
+        'cwst', 'label'
+    )
+), (
+    'cat.marcedit.flateditor', 'gui', 'bool',
+    oils_i18n_gettext(
+        'cat.marcedit.flateditor',
+        'Use Flat MARC Editor',
+        'cwst', 'label'
+    )
+), (
+    'cat.marcedit.stack_subfields', 'gui', 'bool',
+    oils_i18n_gettext(
+        'cat.marcedit.stack_subfields',
+        'MARC Editor Stack Subfields',
+        'cwst', 'label'
+    )
+), (
+    'eg.offline.print_receipt', 'gui', 'bool',
+    oils_i18n_gettext(
+        'eg.offline.print_receipt',
+        'Offline Print Receipt',
+        'cwst', 'label'
+    )
+), (
+    'eg.offline.strict_barcode', 'gui', 'bool',
+    oils_i18n_gettext(
+        'eg.offline.strict_barcode',
+        'Offline Use Strict Barcode',
+        'cwst', 'label'
+    )
+), (
+    'cat.default_bib_marc_template', 'gui', 'string',
+    oils_i18n_gettext(
+        'cat.default_bib_marc_template',
+        'Default MARC Template',
+        'cwst', 'label'
+    )
+), (
+    'eg.audio.disable', 'gui', 'bool',
+    oils_i18n_gettext(
+        'eg.audio.disable',
+        'Disable Staff Client Notification Audio',
+        'cwst', 'label'
+    )
+), (
+    'eg.search.adv_pane', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.search.adv_pane',
+        'Catalog Advanced Search Default Pane',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.bills_current', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.bills_current',
+        'Print Template Context: bills_current',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.bills_current', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.bills_current',
+        'Print Template: bills_current',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.bills_historical', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.bills_historical',
+        'Print Template Context: bills_historical',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.bills_historical', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.bills_historical',
+        'Print Template: bills_historical',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.bill_payment', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.bill_payment',
+        'Print Template Context: bill_payment',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.bill_payment', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.bill_payment',
+        'Print Template: bill_payment',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.checkin', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.checkin',
+        'Print Template Context: checkin',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.checkin', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.checkin',
+        'Print Template: checkin',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.checkout', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.checkout',
+        'Print Template Context: checkout',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.checkout', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.checkout',
+        'Print Template: checkout',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.hold_transit_slip', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.hold_transit_slip',
+        'Print Template Context: hold_transit_slip',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.hold_transit_slip', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.hold_transit_slip',
+        'Print Template: hold_transit_slip',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.hold_shelf_slip', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.hold_shelf_slip',
+        'Print Template Context: hold_shelf_slip',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.hold_shelf_slip', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.hold_shelf_slip',
+        'Print Template: hold_shelf_slip',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.holds_for_bib', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.holds_for_bib',
+        'Print Template Context: holds_for_bib',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.holds_for_bib', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.holds_for_bib',
+        'Print Template: holds_for_bib',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.holds_for_patron', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.holds_for_patron',
+        'Print Template Context: holds_for_patron',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.holds_for_patron', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.holds_for_patron',
+        'Print Template: holds_for_patron',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.hold_pull_list', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.hold_pull_list',
+        'Print Template Context: hold_pull_list',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.hold_pull_list', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.hold_pull_list',
+        'Print Template: hold_pull_list',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.hold_shelf_list', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.hold_shelf_list',
+        'Print Template Context: hold_shelf_list',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.hold_shelf_list', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.hold_shelf_list',
+        'Print Template: hold_shelf_list',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.in_house_use_list', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.in_house_use_list',
+        'Print Template Context: in_house_use_list',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.in_house_use_list', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.in_house_use_list',
+        'Print Template: in_house_use_list',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.item_status', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.item_status',
+        'Print Template Context: item_status',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.item_status', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.item_status',
+        'Print Template: item_status',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.items_out', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.items_out',
+        'Print Template Context: items_out',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.items_out', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.items_out',
+        'Print Template: items_out',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.patron_address', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.patron_address',
+        'Print Template Context: patron_address',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.patron_address', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.patron_address',
+        'Print Template: patron_address',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.patron_data', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.patron_data',
+        'Print Template Context: patron_data',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.patron_data', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.patron_data',
+        'Print Template: patron_data',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.patron_note', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.patron_note',
+        'Print Template Context: patron_note',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.patron_note', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.patron_note',
+        'Print Template: patron_note',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.renew', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.renew',
+        'Print Template Context: renew',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.renew', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.renew',
+        'Print Template: renew',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.transit_list', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.transit_list',
+        'Print Template Context: transit_list',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.transit_list', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.transit_list',
+        'Print Template: transit_list',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.transit_slip', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.transit_slip',
+        'Print Template Context: transit_slip',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.transit_slip', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.transit_slip',
+        'Print Template: transit_slip',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.offline_checkout', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.offline_checkout',
+        'Print Template Context: offline_checkout',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.offline_checkout', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.offline_checkout',
+        'Print Template: offline_checkout',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.offline_renew', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.offline_renew',
+        'Print Template Context: offline_renew',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.offline_renew', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.offline_renew',
+        'Print Template: offline_renew',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.offline_checkin', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.offline_checkin',
+        'Print Template Context: offline_checkin',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.offline_checkin', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.offline_checkin',
+        'Print Template: offline_checkin',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.offline_in_house_use', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.offline_in_house_use',
+        'Print Template Context: offline_in_house_use',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.offline_in_house_use', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.offline_in_house_use',
+        'Print Template: offline_in_house_use',
+        'cwst', 'label'
+    )
+), (
+    'eg.serials.stream_names', 'gui', 'array',
+    oils_i18n_gettext(
+        'eg.serials.stream_names',
+        'Serials Local Stream Names',
+        'cwst', 'label'
+    )
+), (
+    'eg.serials.items.do_print_routing_lists', 'gui', 'bool',
+    oils_i18n_gettext(
+        'eg.serials.items.do_print_routing_lists',
+        'Serials Print Routing Lists',
+        'cwst', 'label'
+    )
+), (
+    'eg.serials.items.receive_and_barcode', 'gui', 'bool',
+    oils_i18n_gettext(
+        'eg.serials.items.receive_and_barcode',
+        'Serials Barcode On Receive',
+        'cwst', 'label'
+    )
+);
+
+
+-- More values with fm_class'es
+INSERT INTO config.workstation_setting_type (name, grp, datatype, fm_class, label)
+VALUES (
+    'eg.search.search_lib', 'gui', 'link', 'aou',
+    oils_i18n_gettext(
+        'eg.search.search_lib',
+        'Staff Catalog Default Search Library',
+        'cwst', 'label'
+    )
+), (
+    'eg.search.pref_lib', 'gui', 'link', 'aou',
+    oils_i18n_gettext(
+        'eg.search.pref_lib',
+        'Staff Catalog Preferred Library',
+        'cwst', 'label'
+    )
+);
+
+
+
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.workstation-settings.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.workstation-settings.sql
new file mode 100644
index 0000000..11ff097
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.workstation-settings.sql
@@ -0,0 +1,227 @@
+BEGIN;
+
+CREATE TYPE actor.cascade_setting_summary AS (
+    name TEXT,
+    value JSON,
+    has_org_setting BOOLEAN,
+    has_user_setting BOOLEAN,
+    has_workstation_setting BOOLEAN
+);
+
+-- SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+
+CREATE TABLE config.workstation_setting_type (
+    name            TEXT    PRIMARY KEY,
+    label           TEXT    UNIQUE NOT NULL,
+    grp             TEXT    REFERENCES config.settings_group (name),
+    description     TEXT,
+    datatype        TEXT    NOT NULL DEFAULT 'string',
+    fm_class        TEXT,
+    --
+    -- define valid datatypes
+    --
+    CONSTRAINT cwst_valid_datatype CHECK ( datatype IN
+    ( 'bool', 'integer', 'float', 'currency', 'interval',
+      'date', 'string', 'object', 'array', 'link' ) ),
+    --
+    -- fm_class is meaningful only for 'link' datatype
+    --
+    CONSTRAINT cwst_no_empty_link CHECK
+    ( ( datatype =  'link' AND fm_class IS NOT NULL ) OR
+      ( datatype <> 'link' AND fm_class IS NULL ) )
+);
+
+CREATE TABLE actor.workstation_setting (
+    id          SERIAL PRIMARY KEY,
+    workstation INT    NOT NULL REFERENCES actor.workstation (id) 
+                       ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    name        TEXT   NOT NULL REFERENCES config.workstation_setting_type (name) 
+                       ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    value       JSON   NOT NULL
+);
+
+
+CREATE INDEX actor_workstation_setting_workstation_idx 
+    ON actor.workstation_setting (workstation);
+
+CREATE OR REPLACE FUNCTION config.setting_is_user_or_ws()
+RETURNS TRIGGER AS $FUNC$
+BEGIN
+
+    IF TG_TABLE_NAME = 'usr_setting_type' THEN
+        PERFORM TRUE FROM config.workstation_setting_type cwst
+            WHERE cwst.name = NEW.name;
+        IF NOT FOUND THEN
+            RETURN NULL;
+        END IF;
+    END IF;
+
+    IF TG_TABLE_NAME = 'workstation_setting_type' THEN
+        PERFORM TRUE FROM config.usr_setting_type cust
+            WHERE cust.name = NEW.name;
+        IF NOT FOUND THEN
+            RETURN NULL;
+        END IF;
+    END IF;
+
+    RAISE EXCEPTION 
+        '% Cannot be used as both a user setting and a workstation setting.', 
+        NEW.name;
+END;
+$FUNC$ LANGUAGE PLPGSQL STABLE;
+
+CREATE CONSTRAINT TRIGGER check_setting_is_usr_or_ws
+  AFTER INSERT OR UPDATE ON config.usr_setting_type
+  FOR EACH ROW EXECUTE PROCEDURE config.setting_is_user_or_ws();
+
+CREATE CONSTRAINT TRIGGER check_setting_is_usr_or_ws
+  AFTER INSERT OR UPDATE ON config.workstation_setting_type
+  FOR EACH ROW EXECUTE PROCEDURE config.setting_is_user_or_ws();
+
+CREATE OR REPLACE FUNCTION actor.get_cascade_setting(
+    setting_name TEXT, org_id INT, user_id INT, workstation_id INT) 
+    RETURNS actor.cascade_setting_summary AS
+$FUNC$
+DECLARE
+    setting_value JSON;
+    summary actor.cascade_setting_summary;
+    org_setting_type config.org_unit_setting_type%ROWTYPE;
+BEGIN
+
+    summary.name := setting_name;
+
+    -- Collect the org setting type status first in case we exit early.
+    -- The existance of an org setting type is not considered
+    -- privileged information.
+    SELECT INTO org_setting_type * 
+        FROM config.org_unit_setting_type WHERE name = setting_name;
+    IF FOUND THEN
+        summary.has_org_setting := TRUE;
+    ELSE
+        summary.has_org_setting := FALSE;
+    END IF;
+
+    -- User and workstation settings have the same priority.
+    -- Start with user settings since that's the simplest code path.
+    -- The workstation_id is ignored if no user_id is provided.
+    IF user_id IS NOT NULL THEN
+
+        SELECT INTO summary.value value FROM actor.usr_setting
+            WHERE usr = user_id AND name = setting_name;
+
+        IF FOUND THEN
+            -- if we have a value, we have a setting type
+            summary.has_user_setting := TRUE;
+
+            IF workstation_id IS NOT NULL THEN
+                -- Only inform the caller about the workstation
+                -- setting type disposition when a workstation id is
+                -- provided.  Otherwise, it's NULL to indicate UNKNOWN.
+                summary.has_workstation_setting := FALSE;
+            END IF;
+
+            RETURN summary;
+        END IF;
+
+        -- no user setting value, but a setting type may exist
+        SELECT INTO summary.has_user_setting EXISTS (
+            SELECT TRUE FROM config.usr_setting_type 
+            WHERE name = setting_name
+        );
+
+        IF workstation_id IS NOT NULL THEN 
+
+            IF NOT summary.has_user_setting THEN
+                -- A workstation setting type may only exist when a user
+                -- setting type does not.
+
+                SELECT INTO summary.value value 
+                    FROM actor.workstation_setting         
+                    WHERE workstation = workstation_id AND name = setting_name;
+
+                IF FOUND THEN
+                    -- if we have a value, we have a setting type
+                    summary.has_workstation_setting := TRUE;
+                    RETURN summary;
+                END IF;
+
+                -- no value, but a setting type may exist
+                SELECT INTO summary.has_workstation_setting EXISTS (
+                    SELECT TRUE FROM config.workstation_setting_type 
+                    WHERE name = setting_name
+                );
+            END IF;
+
+            -- Finally make use of the workstation to determine the org
+            -- unit if none is provided.
+            IF org_id IS NULL AND summary.has_org_setting THEN
+                SELECT INTO org_id owning_lib 
+                    FROM actor.workstation WHERE id = workstation_id;
+            END IF;
+        END IF;
+    END IF;
+
+    -- Some org unit settings are protected by a view permission.
+    -- First see if we have any data that needs protecting, then 
+    -- check the permission if needed.
+
+    IF NOT summary.has_org_setting THEN
+        RETURN summary;
+    END IF;
+
+    -- avoid putting the value into the summary until we confirm
+    -- the value should be visible to the caller.
+    SELECT INTO setting_value value 
+        FROM actor.org_unit_ancestor_setting(setting_name, org_id);
+
+    IF NOT FOUND THEN
+        -- No value found -- perm check is irrelevant.
+        RETURN summary;
+    END IF;
+
+    IF org_setting_type.view_perm IS NOT NULL THEN
+
+        IF user_id IS NULL THEN
+            RAISE NOTICE 'Perm check required but no user_id provided';
+            RETURN summary;
+        END IF;
+
+        IF NOT permission.usr_has_perm(
+            user_id, (SELECT code FROM permission.perm_list 
+                WHERE id = org_setting_type.view_perm), org_id) 
+        THEN
+            RAISE NOTICE 'Perm check failed for user % on %',
+                user_id, org_setting_type.view_perm;
+            RETURN summary;
+        END IF;
+    END IF;
+
+    -- Perm check succeeded or was not necessary.
+    summary.value := setting_value;
+    RETURN summary;
+END;
+$FUNC$ LANGUAGE PLPGSQL;
+
+
+CREATE OR REPLACE FUNCTION actor.get_cascade_setting_batch(
+    setting_names TEXT[], org_id INT, user_id INT, workstation_id INT) 
+    RETURNS SETOF actor.cascade_setting_summary AS
+$FUNC$
+-- Returns a row per setting matching the setting name order.  If no 
+-- value is applied, NULL is returned to retain name-response ordering.
+DECLARE
+    setting_name TEXT;
+    summary actor.cascade_setting_summary;
+BEGIN
+    FOREACH setting_name IN ARRAY setting_names LOOP
+        SELECT INTO summary * FROM actor.get_cascade_setting(
+            setting_Name, org_id, user_id, workstation_id);
+        RETURN NEXT summary;
+    END LOOP;
+END;
+$FUNC$ LANGUAGE PLPGSQL;
+
+COMMIT;
+
+
+
diff --git a/Open-ILS/src/sql/Pg/upgrade/YYYY.data.workstation-settings.sql b/Open-ILS/src/sql/Pg/upgrade/YYYY.data.workstation-settings.sql
new file mode 100644
index 0000000..d50c371
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/upgrade/YYYY.data.workstation-settings.sql
@@ -0,0 +1,893 @@
+BEGIN;
+
+INSERT INTO permission.perm_list (id, code, description) VALUES
+ (607, 'APPLY_WORKSTATION_SETTING',
+   oils_i18n_gettext(607, 'APPLY_WORKSTATION_SETTING', 'ppl', 'description'));
+
+INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
+VALUES (
+    'eg.circ.checkin.no_precat_alert', 'circ', 'bool',
+    oils_i18n_gettext(
+        'eg.circ.checkin.no_precat_alert',
+        'Checkin: Ignore Precataloged Items',
+        'cwst', 'label'
+    )
+), (
+    'eg.circ.checkin.noop', 'circ', 'bool',
+    oils_i18n_gettext(
+        'eg.circ.checkin.noop',
+        'Checkin: Suppress Holds and Transits',
+        'cwst', 'label'
+    )
+), (
+    'eg.circ.checkin.void_overdues', 'circ', 'bool',
+    oils_i18n_gettext(
+        'eg.circ.checkin.void_overdues',
+        'Checkin: Amnesty Mode',
+        'cwst', 'label'
+    )
+), (
+    'eg.circ.checkin.auto_print_holds_transits', 'circ', 'bool',
+    oils_i18n_gettext(
+        'eg.circ.checkin.auto_print_holds_transits',
+        'Checkin: Auto-Print Holds and Transits',
+        'cwst', 'label'
+    )
+), (
+    'eg.circ.checkin.clear_expired', 'circ', 'bool',
+    oils_i18n_gettext(
+        'eg.circ.checkin.clear_expired',
+        'Checkin: Clear Holds Shelf',
+        'cwst', 'label'
+    )
+), (
+    'eg.circ.checkin.retarget_holds', 'circ', 'bool',
+    oils_i18n_gettext(
+        'eg.circ.checkin.retarget_holds',
+        'Checkin: Retarget Local Holds',
+        'cwst', 'label'
+    )
+), (
+    'eg.circ.checkin.retarget_holds_all', 'circ', 'bool',
+    oils_i18n_gettext(
+        'eg.circ.checkin.retarget_holds_all',
+        'Checkin: Retarget All Statuses',
+        'cwst', 'label'
+    )
+), (
+    'eg.circ.checkin.hold_as_transit', 'circ', 'bool',
+    oils_i18n_gettext(
+        'eg.circ.checkin.hold_as_transit',
+        'Checkin: Capture Local Holds as Transits',
+        'cwst', 'label'
+    )
+), (
+    'eg.circ.checkin.manual_float', 'circ', 'bool',
+    oils_i18n_gettext(
+        'eg.circ.checkin.manual_float',
+        'Checkin: Manual Floating Active',
+        'cwst', 'label'
+    )
+), (
+    'eg.circ.patron.summary.collapse', 'circ', 'bool',
+    oils_i18n_gettext(
+        'eg.circ.patron.summary.collapse',
+        'Collaps Patron Summary Display',
+        'cwst', 'label'
+    )
+), (
+    'circ.bills.receiptonpay', 'circ', 'bool',
+    oils_i18n_gettext(
+        'circ.bills.receiptonpay',
+        'Print Receipt On Payment',
+        'cwst', 'label'
+    )
+), (
+    'circ.renew.strict_barcode', 'circ', 'bool',
+    oils_i18n_gettext(
+        'circ.renew.strict_barcode',
+        'Renew: Strict Barcode',
+        'cwst', 'label'
+    )
+), (
+    'circ.checkin.strict_barcode', 'circ', 'bool',
+    oils_i18n_gettext(
+        'circ.checkin.strict_barcode',
+        'Checkin: Strict Barcode',
+        'cwst', 'label'
+    )
+), (
+    'cat.holdings_show_copies', 'cat', 'bool',
+    oils_i18n_gettext(
+        'cat.holdings_show_copies',
+        'Holdings View Show Copies',
+        'cwst', 'label'
+    )
+), (
+    'cat.holdings_show_empty', 'cat', 'bool',
+    oils_i18n_gettext(
+        'cat.holdings_show_empty',
+        'Holdings View Show Empty Volumes',
+        'cwst', 'label'
+    )
+), (
+    'cat.holdings_show_vols', 'cat', 'bool',
+    oils_i18n_gettext(
+        'cat.holdings_show_vols',
+        'Holdings View Show Volumes',
+        'cwst', 'label'
+    )
+), (
+    'eg.circ.patron.search.include_inactive', 'circ', 'bool',
+    oils_i18n_gettext(
+        'eg.circ.patron.search.include_inactive',
+        'Patron Search Include Inactive',
+        'cwst', 'label'
+    )
+), (
+    'eg.circ.patron.search.show_extras', 'circ', 'bool',
+    oils_i18n_gettext(
+        'eg.circ.patron.search.show_extras',
+        'Patron Search Show Extra Search Options',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.checkin.checkin', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.checkin.checkin',
+        'Grid Config: circ.checkin.checkin',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.checkin.capture', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.checkin.capture',
+        'Grid Config: circ.checkin.capture',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.server.config.copy_tag_type', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.admin.server.config.copy_tag_type',
+        'Grid Config: admin.server.config.copy_tag_type',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.server.config.metabib_field_virtual_map.grid', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.admin.server.config.metabib_field_virtual_map.grid',
+        'Grid Config: admin.server.config.metabib_field_virtual_map.grid',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.server.config.metabib_field.grid', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.admin.server.config.metabib_field.grid',
+        'Grid Config: admin.server.config.metabib_field.grid',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.server.config.marc_field', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.admin.server.config.marc_field',
+        'Grid Config: admin.server.config.marc_field',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.server.asset.copy_tag', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.admin.server.asset.copy_tag',
+        'Grid Config: admin.server.asset.copy_tag',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.local.circ.neg_balance_users', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.admin.local.circ.neg_balance_users',
+        'Grid Config: admin.local.circ.neg_balance_users',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.local.rating.badge', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.admin.local.rating.badge',
+        'Grid Config: admin.local.rating.badge',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.workstation.work_log', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.admin.workstation.work_log',
+        'Grid Config: admin.workstation.work_log',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.workstation.patron_log', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.admin.workstation.patron_log',
+        'Grid Config: admin.workstation.patron_log',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.serials.pattern_template', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.admin.serials.pattern_template',
+        'Grid Config: admin.serials.pattern_template',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.serials.copy_templates', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.serials.copy_templates',
+        'Grid Config: serials.copy_templates',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.cat.record_overlay.holdings', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.record_overlay.holdings',
+        'Grid Config: cat.record_overlay.holdings',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.cat.bucket.record.search', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.bucket.record.search',
+        'Grid Config: cat.bucket.record.search',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.cat.bucket.record.view', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.bucket.record.view',
+        'Grid Config: cat.bucket.record.view',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.cat.bucket.record.pending', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.bucket.record.pending',
+        'Grid Config: cat.bucket.record.pending',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.cat.bucket.copy.view', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.bucket.copy.view',
+        'Grid Config: cat.bucket.copy.view',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.cat.bucket.copy.pending', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.bucket.copy.pending',
+        'Grid Config: cat.bucket.copy.pending',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.cat.items', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.items',
+        'Grid Config: cat.items',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.cat.volcopy.copies', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.volcopy.copies',
+        'Grid Config: cat.volcopy.copies',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.cat.volcopy.copies.complete', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.volcopy.copies.complete',
+        'Grid Config: cat.volcopy.copies.complete',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.cat.peer_bibs', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.peer_bibs',
+        'Grid Config: cat.peer_bibs',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.cat.catalog.holds', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.catalog.holds',
+        'Grid Config: cat.catalog.holds',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.cat.holdings', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.holdings',
+        'Grid Config: cat.holdings',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.cat.z3950_results', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.cat.z3950_results',
+        'Grid Config: cat.z3950_results',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.holds.shelf', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.holds.shelf',
+        'Grid Config: circ.holds.shelf',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.holds.pull', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.holds.pull',
+        'Grid Config: circ.holds.pull',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.in_house_use', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.in_house_use',
+        'Grid Config: circ.in_house_use',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.renew', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.renew',
+        'Grid Config: circ.renew',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.transits.list', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.transits.list',
+        'Grid Config: circ.transits.list',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.patron.holds', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.patron.holds',
+        'Grid Config: circ.patron.holds',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.pending_patrons.list', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.pending_patrons.list',
+        'Grid Config: circ.pending_patrons.list',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.patron.items_out.noncat', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.patron.items_out.noncat',
+        'Grid Config: circ.patron.items_out.noncat',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.patron.items_out', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.patron.items_out',
+        'Grid Config: circ.patron.items_out',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.patron.billhistory_payments', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.patron.billhistory_payments',
+        'Grid Config: circ.patron.billhistory_payments',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.user.bucket.view', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.user.bucket.view',
+        'Grid Config: user.bucket.view',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.user.bucket.pending', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.user.bucket.pending',
+        'Grid Config: user.bucket.pending',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.patron.staff_messages', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.patron.staff_messages',
+        'Grid Config: circ.patron.staff_messages',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.patron.archived_messages', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.patron.archived_messages',
+        'Grid Config: circ.patron.archived_messages',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.patron.bills', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.patron.bills',
+        'Grid Config: circ.patron.bills',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.patron.checkout', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.patron.checkout',
+        'Grid Config: circ.patron.checkout',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.serials.mfhd_grid', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.serials.mfhd_grid',
+        'Grid Config: serials.mfhd_grid',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.serials.view_item_grid', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.serials.view_item_grid',
+        'Grid Config: serials.view_item_grid',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.serials.dist_stream_grid', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.serials.dist_stream_grid',
+        'Grid Config: serials.dist_stream_grid',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.circ.patron.search', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.circ.patron.search',
+        'Grid Config: circ.patron.search',
+        'cwst', 'label'
+    )
+), (
+    'eg.cat.record.summary.collapse', 'gui', 'bool',
+    oils_i18n_gettext(
+        'eg.cat.record.summary.collapse',
+        'Collapse Bib Record Summary',
+        'cwst', 'label'
+    )
+), (
+    'cat.marcedit.flateditor', 'gui', 'bool',
+    oils_i18n_gettext(
+        'cat.marcedit.flateditor',
+        'Use Flat MARC Editor',
+        'cwst', 'label'
+    )
+), (
+    'cat.marcedit.stack_subfields', 'gui', 'bool',
+    oils_i18n_gettext(
+        'cat.marcedit.stack_subfields',
+        'MARC Editor Stack Subfields',
+        'cwst', 'label'
+    )
+), (
+    'eg.offline.print_receipt', 'gui', 'bool',
+    oils_i18n_gettext(
+        'eg.offline.print_receipt',
+        'Offline Print Receipt',
+        'cwst', 'label'
+    )
+), (
+    'eg.offline.strict_barcode', 'gui', 'bool',
+    oils_i18n_gettext(
+        'eg.offline.strict_barcode',
+        'Offline Use Strict Barcode',
+        'cwst', 'label'
+    )
+), (
+    'cat.default_bib_marc_template', 'gui', 'string',
+    oils_i18n_gettext(
+        'cat.default_bib_marc_template',
+        'Default MARC Template',
+        'cwst', 'label'
+    )
+), (
+    'eg.audio.disable', 'gui', 'bool',
+    oils_i18n_gettext(
+        'eg.audio.disable',
+        'Disable Staff Client Notification Audio',
+        'cwst', 'label'
+    )
+), (
+    'eg.search.adv_pane', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.search.adv_pane',
+        'Catalog Advanced Search Default Pane',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.bills_current', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.bills_current',
+        'Print Template Context: bills_current',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.bills_current', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.bills_current',
+        'Print Template: bills_current',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.bills_historical', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.bills_historical',
+        'Print Template Context: bills_historical',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.bills_historical', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.bills_historical',
+        'Print Template: bills_historical',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.bill_payment', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.bill_payment',
+        'Print Template Context: bill_payment',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.bill_payment', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.bill_payment',
+        'Print Template: bill_payment',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.checkin', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.checkin',
+        'Print Template Context: checkin',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.checkin', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.checkin',
+        'Print Template: checkin',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.checkout', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.checkout',
+        'Print Template Context: checkout',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.checkout', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.checkout',
+        'Print Template: checkout',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.hold_transit_slip', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.hold_transit_slip',
+        'Print Template Context: hold_transit_slip',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.hold_transit_slip', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.hold_transit_slip',
+        'Print Template: hold_transit_slip',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.hold_shelf_slip', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.hold_shelf_slip',
+        'Print Template Context: hold_shelf_slip',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.hold_shelf_slip', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.hold_shelf_slip',
+        'Print Template: hold_shelf_slip',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.holds_for_bib', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.holds_for_bib',
+        'Print Template Context: holds_for_bib',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.holds_for_bib', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.holds_for_bib',
+        'Print Template: holds_for_bib',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.holds_for_patron', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.holds_for_patron',
+        'Print Template Context: holds_for_patron',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.holds_for_patron', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.holds_for_patron',
+        'Print Template: holds_for_patron',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.hold_pull_list', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.hold_pull_list',
+        'Print Template Context: hold_pull_list',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.hold_pull_list', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.hold_pull_list',
+        'Print Template: hold_pull_list',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.hold_shelf_list', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.hold_shelf_list',
+        'Print Template Context: hold_shelf_list',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.hold_shelf_list', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.hold_shelf_list',
+        'Print Template: hold_shelf_list',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.in_house_use_list', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.in_house_use_list',
+        'Print Template Context: in_house_use_list',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.in_house_use_list', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.in_house_use_list',
+        'Print Template: in_house_use_list',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.item_status', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.item_status',
+        'Print Template Context: item_status',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.item_status', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.item_status',
+        'Print Template: item_status',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.items_out', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.items_out',
+        'Print Template Context: items_out',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.items_out', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.items_out',
+        'Print Template: items_out',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.patron_address', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.patron_address',
+        'Print Template Context: patron_address',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.patron_address', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.patron_address',
+        'Print Template: patron_address',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.patron_data', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.patron_data',
+        'Print Template Context: patron_data',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.patron_data', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.patron_data',
+        'Print Template: patron_data',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.patron_note', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.patron_note',
+        'Print Template Context: patron_note',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.patron_note', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.patron_note',
+        'Print Template: patron_note',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.renew', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.renew',
+        'Print Template Context: renew',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.renew', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.renew',
+        'Print Template: renew',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.transit_list', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.transit_list',
+        'Print Template Context: transit_list',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.transit_list', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.transit_list',
+        'Print Template: transit_list',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.transit_slip', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.transit_slip',
+        'Print Template Context: transit_slip',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.transit_slip', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.transit_slip',
+        'Print Template: transit_slip',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.offline_checkout', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.offline_checkout',
+        'Print Template Context: offline_checkout',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.offline_checkout', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.offline_checkout',
+        'Print Template: offline_checkout',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.offline_renew', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.offline_renew',
+        'Print Template Context: offline_renew',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.offline_renew', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.offline_renew',
+        'Print Template: offline_renew',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.offline_checkin', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.offline_checkin',
+        'Print Template Context: offline_checkin',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.offline_checkin', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.offline_checkin',
+        'Print Template: offline_checkin',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template_context.offline_in_house_use', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template_context.offline_in_house_use',
+        'Print Template Context: offline_in_house_use',
+        'cwst', 'label'
+    )
+), (
+    'eg.print.template.offline_in_house_use', 'gui', 'string',
+    oils_i18n_gettext(
+        'eg.print.template.offline_in_house_use',
+        'Print Template: offline_in_house_use',
+        'cwst', 'label'
+    )
+), (
+    'eg.serials.stream_names', 'gui', 'array',
+    oils_i18n_gettext(
+        'eg.serials.stream_names',
+        'Serials Local Stream Names',
+        'cwst', 'label'
+    )
+), (
+    'eg.serials.items.do_print_routing_lists', 'gui', 'bool',
+    oils_i18n_gettext(
+        'eg.serials.items.do_print_routing_lists',
+        'Serials Print Routing Lists',
+        'cwst', 'label'
+    )
+), (
+    'eg.serials.items.receive_and_barcode', 'gui', 'bool',
+    oils_i18n_gettext(
+        'eg.serials.items.receive_and_barcode',
+        'Serials Barcode On Receive',
+        'cwst', 'label'
+    )
+);
+
+
+-- More values with fm_class'es
+INSERT INTO config.workstation_setting_type (name, grp, datatype, fm_class, label)
+VALUES (
+    'eg.search.search_lib', 'gui', 'link', 'aou',
+    oils_i18n_gettext(
+        'eg.search.search_lib',
+        'Staff Catalog Default Search Library',
+        'cwst', 'label'
+    )
+), (
+    'eg.search.pref_lib', 'gui', 'link', 'aou',
+    oils_i18n_gettext(
+        'eg.search.pref_lib',
+        'Staff Catalog Preferred Library',
+        'cwst', 'label'
+    )
+);
+
+
+COMMIT;
+
+
diff --git a/Open-ILS/src/templates/staff/admin/workstation/index.tt2 b/Open-ILS/src/templates/staff/admin/workstation/index.tt2
index 44842ea..3f34c1f 100644
--- a/Open-ILS/src/templates/staff/admin/workstation/index.tt2
+++ b/Open-ILS/src/templates/staff/admin/workstation/index.tt2
@@ -23,6 +23,8 @@ angular.module('egCoreMod').run(['egStrings', function(s) {
   s.PRINT_TEMPLATES_FAIL_IMPORT = "[% l('Failed to import any print template(s)') %]";
   s.HATCH_SETTINGS_MIGRATION_SUCCESS = "[% l('Settings successfully migrated') %]";
   s.HATCH_SETTINGS_MIGRATION_FAILURE = "[% l('Settings migration failed') %]";
+  s.HATCH_SERVER_SETTINGS_MIGRATION_CONFIRM = 
+    "[% l('This will delete the local version all settings configured to live on the server.  Continue?') %]";
 }]);
 </script>
 [% END %]
diff --git a/Open-ILS/web/js/ui/default/staff/admin/workstation/app.js b/Open-ILS/web/js/ui/default/staff/admin/workstation/app.js
index 083b5dd..98dc9b7 100644
--- a/Open-ILS/web/js/ui/default/staff/admin/workstation/app.js
+++ b/Open-ILS/web/js/ui/default/staff/admin/workstation/app.js
@@ -785,7 +785,9 @@ function($scope , $q , egCore , egConfirmDialog) {
                         egCore.hatch.removeLocalItem(key);
                         refreshKeys();
                     } else {
-                        egCore.hatch.removeItem(key)
+                        // Honor requests to remove items from Hatch even
+                        // when Hatch is configured for data storage.
+                        egCore.hatch.removeRemoteItem(key)
                         .then(function() { refreshKeys() });
                     }
                 },
diff --git a/Open-ILS/web/js/ui/default/staff/circ/checkin/app.js b/Open-ILS/web/js/ui/default/staff/circ/checkin/app.js
index 6a79396..103a8e0 100644
--- a/Open-ILS/web/js/ui/default/staff/circ/checkin/app.js
+++ b/Open-ILS/web/js/ui/default/staff/circ/checkin/app.js
@@ -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,15 @@ 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 });
+    var snames = modifiers.map(function(m) {return 'eg.circ.checkin.' + m;});
+    egCore.hatch.getItemBatch(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
diff --git a/Open-ILS/web/js/ui/default/staff/services/hatch.js b/Open-ILS/web/js/ui/default/staff/services/hatch.js
index f7083a7..d35c3f1 100644
--- a/Open-ILS/web/js/ui/default/staff/services/hatch.js
+++ b/Open-ILS/web/js/ui/default/staff/services/hatch.js
@@ -25,18 +25,24 @@
 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;

-----------------------------------------------------------------------

Summary of changes:
 Open-ILS/examples/fm_IDL.xml                       |   46 +-
 Open-ILS/src/perlmods/MANIFEST                     |    1 +
 .../src/perlmods/lib/OpenILS/Application/Actor.pm  |    1 +
 .../lib/OpenILS/Application/Actor/Settings.pm      |  318 +++++++
 Open-ILS/src/sql/Pg/002.schema.config.sql          |   60 ++-
 Open-ILS/src/sql/Pg/005.schema.actors.sql          |  164 ++++
 Open-ILS/src/sql/Pg/950.data.seed-values.sql       |  930 +++++++++++++++++++-
 .../sql/Pg/t/lp1750894-workststation-settings.pg   |   84 ++
 .../upgrade/1116.schema.workstation-settings.sql   |  227 +++++
 .../Pg/upgrade/1117.data.workstation-settings.sql  |  930 ++++++++++++++++++++
 .../templates/staff/admin/workstation/index.tt2    |    2 +
 .../staff/admin/workstation/t_stored_prefs.tt2     |    7 +-
 .../js/ui/default/staff/admin/workstation/app.js   |   20 +-
 .../web/js/ui/default/staff/circ/checkin/app.js    |   13 +-
 Open-ILS/web/js/ui/default/staff/services/grid.js  |   46 +-
 Open-ILS/web/js/ui/default/staff/services/hatch.js |  400 ++++++++-
 .../Client/workstation-server-settings.adoc        |   71 ++
 17 files changed, 3264 insertions(+), 56 deletions(-)
 create mode 100644 Open-ILS/src/perlmods/lib/OpenILS/Application/Actor/Settings.pm
 create mode 100644 Open-ILS/src/sql/Pg/t/lp1750894-workststation-settings.pg
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/1116.schema.workstation-settings.sql
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/1117.data.workstation-settings.sql
 create mode 100644 docs/RELEASE_NOTES_NEXT/Client/workstation-server-settings.adoc


hooks/post-receive
-- 
Evergreen ILS




More information about the open-ils-commits mailing list