LP#1750894 Workstation & Cascade settings
authorBill Erickson <berickxx@gmail.com>
Tue, 13 Mar 2018 18:06:23 +0000 (14:06 -0400)
committerKathy Lussier <klussier@masslnc.org>
Fri, 3 Aug 2018 17:20:59 +0000 (13:20 -0400)
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@gmail.com>
Signed-off-by: Kathy Lussier <klussier@masslnc.org>
13 files changed:
Open-ILS/examples/fm_IDL.xml
Open-ILS/src/perlmods/MANIFEST
Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Actor/Settings.pm [new file with mode: 0644]
Open-ILS/src/sql/Pg/002.schema.config.sql
Open-ILS/src/sql/Pg/005.schema.actors.sql
Open-ILS/src/sql/Pg/950.data.seed-values.sql
Open-ILS/src/sql/Pg/upgrade/XXXX.schema.workstation-settings.sql [new file with mode: 0644]
Open-ILS/src/sql/Pg/upgrade/YYYY.data.workstation-settings.sql [new file with mode: 0644]
Open-ILS/src/templates/staff/admin/workstation/index.tt2
Open-ILS/web/js/ui/default/staff/admin/workstation/app.js
Open-ILS/web/js/ui/default/staff/circ/checkin/app.js
Open-ILS/web/js/ui/default/staff/services/hatch.js

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