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

Evergreen Git git at git.evergreen-ils.org
Mon Sep 14 15:57:16 EDT 2015


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

The branch, master has been updated
       via  a9fe4a8772bea60e0b8a22ef070f0ba28732860b (commit)
       via  96fdd22910a21035997fd927a317b58c1ac663ca (commit)
       via  6651101e74c012c1e9df2c013be00e62a70dec80 (commit)
       via  61b801d6b75fff05f78bae4b607b91eaec239ba8 (commit)
       via  e728e2eff65464f9591d44f292bd82ba601703e8 (commit)
       via  707d35fd7559b3a0d2d820d991c8f03678cd73e1 (commit)
       via  183f9c30ac1fbb3a4c6895613f5b2e3214102f60 (commit)
       via  ff118a10373330832863df1eb4b77b9b729f9192 (commit)
       via  e160112ac7a048ec525803ad49b10020c3a85590 (commit)
       via  96e6db3fa5d7fef0ff0bb2c6ca8d73e765104036 (commit)
       via  d656cb4083dabb6f85d49f4098bc649375c04a4f (commit)
       via  2b278e30e4c9dee5d9c1c58915bcff30abb6d5bd (commit)
       via  a2d301fc77ebd55605c1157b058e1790057f447b (commit)
       via  da57d6c3ddd4c1992de290a7812771dec2be351b (commit)
       via  7d2d8cf109fbaa85242ac981e72319286ac06237 (commit)
       via  a20da8af3762303f913b6f86baffe299cab00ed8 (commit)
       via  9c6d3c2f9d0e559c806988d8013fd0dd98e39a22 (commit)
       via  de538a7b15bc0736c66667115807fc7205cc79e5 (commit)
       via  cb532decc7af7a88d15453fcef7d7d0ab6a31c4c (commit)
       via  4d82cd7a9f306855008b0b6e3d1797c75d340d90 (commit)
       via  bd9a659e3c9ded9b8880cf68bc817eca2e749997 (commit)
       via  68eb69e80578ad94699739dc74ebf1748a98658c (commit)
       via  ba4b2921846e66a4fc22ca2ccd7a21a818299ef2 (commit)
       via  1c95ebadc6c60b5a977bcc7cca11eb1e65b31217 (commit)
       via  17607305ba338bdfca3200038cfe1add87fd384e (commit)
       via  00cfa3bb3aed1c7676bb1a62b210e57a09236ff1 (commit)
       via  2ed294bd548d5031ac1b6099077a581754f12daf (commit)
       via  bac309deb5de1013ddbeb7e5e516fde9f99cc461 (commit)
       via  5a91eb9905ae934ede77bbdcd3f919f8349d264b (commit)
       via  58eae9ebb53d65da27cbf9aac9e497069dd69833 (commit)
       via  00815ad5e90a6919c7dae24b8dfdf46a5b592685 (commit)
       via  863e9ea11b3f1528fbc7ccbab24716aadb7a351f (commit)
       via  f6eb26fa807c3ad5f0c01f1271313daca98efc0f (commit)
       via  ebbf072d1e69cf0a255ad60bfc2fdecf979a41ab (commit)
       via  cc3c57a0fa39b681d76a93488927151aaaa89f71 (commit)
       via  417e75f2120f1d31df4c355e5af3f808ef9d7e5e (commit)
       via  9f82aee9c55620c2f1e11cff350951b37a5d336c (commit)
       via  ddc995c9d848c17932b5b32ff976362d634c5476 (commit)
       via  2e9b5949998f189d044f897b8059a9b411b72f46 (commit)
       via  1528a147c4f703fff606a93fff23d3b5c2327705 (commit)
       via  3e078a7077198058542b8aa71b02f2393081f139 (commit)
       via  043ae87a2015abcd73c1b5da447b2da90598b23d (commit)
       via  5171ee499de09bf08772231e183191d77b66031a (commit)
       via  bc2713b733da00427932d1be1a562349dccdd945 (commit)
       via  3e08b6467615261cb194c8f5573867f4e6c7b55f (commit)
       via  13d664a679e7af33129e6600887cb32c3adf3134 (commit)
       via  6970735361eecb13327d61ac3437e99fbac128cd (commit)
       via  208ac82a84dae508952183e58e394cf877bf1b46 (commit)
       via  1e61a8e9e1bc9b32f1d2fa502a376c54e6681c0f (commit)
       via  564ed056e6dc77809ebb720c0f0d0040e904c7e0 (commit)
       via  44e8fc74d8d56fc0623129113a8dfd3acfb48e5b (commit)
       via  cd9051bdb657ebbf4583f8208dce40809ba802f6 (commit)
       via  803bfe7e91035e00818f8c609f8c59c633048e48 (commit)
       via  8071785b4e9588e1aae8aac9aaa1e071448216a3 (commit)
       via  bacc90afece1503dd701df2bb6b1f7270c26c7a3 (commit)
       via  f9f04b4eb71cedf9e866b99a8ac5a9dfa207f218 (commit)
       via  3530113c1acfde66e45a3f67ba65ca43de52c2a4 (commit)
       via  e2f6cca04f506e0500cbddb80806c87f12b91edb (commit)
       via  3b89ef139e2d98783493b9a7e134fd34a3ea5fed (commit)
       via  e089f5acf6626af27e0cc0263f3c977988c5e709 (commit)
       via  2d73d219844e8bb94656afe22e9576cb21edd5ec (commit)
       via  643e236766ffbfe2e7e5baaf7275620e506248e4 (commit)
       via  ee918900a91d239d0b58f190c5b3984408326982 (commit)
       via  4805efa9b793a0ac7021071a95815bda15d758c7 (commit)
       via  14554202c66931a2db463202cd5cfe1680174313 (commit)
       via  b85c76647e5dd8dda64ab663e666bc9cd08da80f (commit)
       via  c9efa31128635fdc7457a67a5eb25149637d31f7 (commit)
       via  45c87bcecca7f73bd27bb5b8190672c7cadbb174 (commit)
       via  ea38b91ea173f009aef4875a9d7732847bf75673 (commit)
       via  59b8da708b411997900c21fc8f551d8efb2ffe08 (commit)
       via  2f33494db6ee17fed89b4fa48772cbb51f6011a4 (commit)
       via  db9b0dff87cc8ad5a2ff97b60f418eee77c2e54d (commit)
       via  acce2de94fa82da4608faf4ec36f922df344a6aa (commit)
       via  557ecb97fb98bc88980db94cf223d9d18aed7e69 (commit)
       via  573dd9fca6e8a4249a2ed38ffea0a37ae70c7426 (commit)
       via  896d2f5aa4dc21acf70257f277273adb7ebb8d7a (commit)
       via  1213ce7265dba43620ef31997e470ee1e444cf44 (commit)
       via  a80485baefde89e1c9dd8c3fe712e8c3a11f1f05 (commit)
       via  53fa1c6856e16e0b8567cdd6056031faa9762fc1 (commit)
       via  aee3eb930e3bda9336a0d36342cee43e7e36befe (commit)
       via  f8b9fbc203732300ba7bef3da3bf1eaac3f1cafd (commit)
       via  7c2b5a004f2490e41a1142e460d8fa81929f78a7 (commit)
       via  cee5310eb751aed14670d68e8a524480bc94cb52 (commit)
       via  c9a2dbf3e8894062b4b2fb7677aa0a4a80d6f2f4 (commit)
       via  0c085fd8662cfc0e2c3a436cba1ba6f592c437ee (commit)
       via  8adca92befae05371a5707f933fe91efd7450692 (commit)
       via  b1f25d1d3b93617adb6e498c5960c87db4038a01 (commit)
       via  85c199c63984620cd1a8ad31d112492c61d1b4c4 (commit)
       via  99a17c4ad454ec4eac3394bb224bb3ad738fdf8f (commit)
       via  30514d239eb4d7860ed27f202bd4e9d30c1ad411 (commit)
       via  dbf2a4c73bc42ff23921d3ae757d648d3b0ef924 (commit)
       via  00d828090abc0dfe6d72525e91aba4b61cfc51ab (commit)
       via  3491b0b9fb27cedf215aa962657740055c6f4f61 (commit)
       via  b962aa9a3ee153835a89d6f325198d50011337cf (commit)
       via  7a051b66f74cdcb85c551a1b02555d64cfc09957 (commit)
       via  6059f36449888a5c5604e92cea4e54da64f1c118 (commit)
       via  52fc0b11335ec839f0a607524f6e1ecee26e4442 (commit)
       via  695c0935b328f96ac992bab3d75786dece7e9380 (commit)
       via  24380867ab3cca203ab3768822f4d3393ec76dc6 (commit)
       via  73c91cca7b0b55cb30a6b29a51e7afdaa0135b1a (commit)
      from  23f86797954e0fb5153a9eea985cdf23eaf75011 (commit)

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

- Log -----------------------------------------------------------------
commit a9fe4a8772bea60e0b8a22ef070f0ba28732860b
Author: Jason Stephenson <jstephenson at mvlc.org>
Date:   Mon Sep 14 15:55:26 2015 -0400

    LP 1495509: Stamping upgrade scripts.
    
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/sql/Pg/002.schema.config.sql b/Open-ILS/src/sql/Pg/002.schema.config.sql
index c5d3d44..3210bea 100644
--- a/Open-ILS/src/sql/Pg/002.schema.config.sql
+++ b/Open-ILS/src/sql/Pg/002.schema.config.sql
@@ -91,7 +91,7 @@ CREATE TRIGGER no_overlapping_deps
     BEFORE INSERT OR UPDATE ON config.db_patch_dependencies
     FOR EACH ROW EXECUTE PROCEDURE evergreen.array_overlap_check ('deprecates');
 
-INSERT INTO config.upgrade_log (version, applied_to) VALUES ('0941', :eg_version); -- yboston/dyrcona
+INSERT INTO config.upgrade_log (version, applied_to) VALUES ('0943', :eg_version); -- gmcarlt/dyrcona
 
 CREATE TABLE config.bib_source (
 	id		SERIAL	PRIMARY KEY,
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.filter_authority_browse_search_by_thesaurus.sql b/Open-ILS/src/sql/Pg/upgrade/0942.schema.filter_authority_browse_search_by_thesaurus.sql
similarity index 99%
rename from Open-ILS/src/sql/Pg/upgrade/XXXX.schema.filter_authority_browse_search_by_thesaurus.sql
rename to Open-ILS/src/sql/Pg/upgrade/0942.schema.filter_authority_browse_search_by_thesaurus.sql
index b7f2be6..c1b9dab 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.filter_authority_browse_search_by_thesaurus.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/0942.schema.filter_authority_browse_search_by_thesaurus.sql
@@ -1,6 +1,6 @@
 BEGIN;
 
--- SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+SELECT evergreen.upgrade_deps_block_check('0942', :eg_version);
 
 CREATE OR REPLACE FUNCTION authority.extract_thesaurus( marcxml TEXT ) RETURNS TEXT AS $func$
 DECLARE
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.additional_authority_fixed_fields.sql b/Open-ILS/src/sql/Pg/upgrade/0943.data.additional_authority_fixed_fields.sql
similarity index 97%
rename from Open-ILS/src/sql/Pg/upgrade/XXXX.data.additional_authority_fixed_fields.sql
rename to Open-ILS/src/sql/Pg/upgrade/0943.data.additional_authority_fixed_fields.sql
index 2e220bd..7a888d1 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.additional_authority_fixed_fields.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/0943.data.additional_authority_fixed_fields.sql
@@ -1,6 +1,6 @@
 BEGIN;
 
--- SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+SELECT evergreen.upgrade_deps_block_check('0943', :eg_version);
 
 INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('Type', 'ldr', 'AUT', 6, 1, 'z');
 INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('GeoDiv', '008', 'AUT', 6, 1, ' ');

commit 96fdd22910a21035997fd927a317b58c1ac663ca
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Mon Sep 14 15:34:47 2015 +0000

    LP#1489955: tweak to work on PostgreSQL 9.1
    
    Stored functions written in SQL cannot use parameter
    names in their bodies until Pg 9.2.
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/sql/Pg/011.schema.authority.sql b/Open-ILS/src/sql/Pg/011.schema.authority.sql
index 014b731..8322867 100644
--- a/Open-ILS/src/sql/Pg/011.schema.authority.sql
+++ b/Open-ILS/src/sql/Pg/011.schema.authority.sql
@@ -884,9 +884,9 @@ CREATE OR REPLACE FUNCTION authority.simple_heading_search_rank( atag_list INT[]
             plainto_tsquery('keyword'::regconfig,$2) ptsq(term)
       WHERE ash.atag = ANY ($1)
             AND ash.index_vector @@ ptsq.term
-            AND CASE thesauruses
+            AND CASE $5
                 WHEN '' THEN TRUE
-                ELSE ash.thesaurus = ANY(regexp_split_to_array(thesauruses, ','))
+                ELSE ash.thesaurus = ANY(regexp_split_to_array($5, ','))
                 END
       ORDER BY ts_rank_cd(ash.index_vector,ptsq.term,14)::numeric
                     + CASE WHEN ash.sort_value LIKE t.term || '%' THEN 2 ELSE 0 END
@@ -927,9 +927,9 @@ CREATE OR REPLACE FUNCTION authority.simple_heading_search_heading( atag_list IN
             plainto_tsquery('keyword'::regconfig,$2) ptsq(term)
       WHERE ash.atag = ANY ($1)
             AND ash.index_vector @@ ptsq.term
-            AND CASE thesauruses
+            AND CASE $5
                 WHEN '' THEN TRUE
-                ELSE ash.thesaurus = ANY(regexp_split_to_array(thesauruses, ','))
+                ELSE ash.thesaurus = ANY(regexp_split_to_array($5, ','))
                 END
       ORDER BY ash.sort_value
       LIMIT $4
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.filter_authority_browse_search_by_thesaurus.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.filter_authority_browse_search_by_thesaurus.sql
index b9084ec..b7f2be6 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.filter_authority_browse_search_by_thesaurus.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.filter_authority_browse_search_by_thesaurus.sql
@@ -514,9 +514,9 @@ CREATE OR REPLACE FUNCTION authority.simple_heading_search_rank( atag_list INT[]
             plainto_tsquery('keyword'::regconfig,$2) ptsq(term)
       WHERE ash.atag = ANY ($1)
             AND ash.index_vector @@ ptsq.term
-            AND CASE thesauruses
+            AND CASE $5
                 WHEN '' THEN TRUE
-                ELSE ash.thesaurus = ANY(regexp_split_to_array(thesauruses, ','))
+                ELSE ash.thesaurus = ANY(regexp_split_to_array($5, ','))
                 END
       ORDER BY ts_rank_cd(ash.index_vector,ptsq.term,14)::numeric
                     + CASE WHEN ash.sort_value LIKE t.term || '%' THEN 2 ELSE 0 END
@@ -557,9 +557,9 @@ CREATE OR REPLACE FUNCTION authority.simple_heading_search_heading( atag_list IN
             plainto_tsquery('keyword'::regconfig,$2) ptsq(term)
       WHERE ash.atag = ANY ($1)
             AND ash.index_vector @@ ptsq.term
-            AND CASE thesauruses
+            AND CASE $5
                 WHEN '' THEN TRUE
-                ELSE ash.thesaurus = ANY(regexp_split_to_array(thesauruses, ','))
+                ELSE ash.thesaurus = ANY(regexp_split_to_array($5, ','))
                 END
       ORDER BY ash.sort_value
       LIMIT $4

commit 6651101e74c012c1e9df2c013be00e62a70dec80
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Mon Sep 14 13:08:12 2015 +0000

    webstaff: update release notes for sprint 2
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/docs/RELEASE_NOTES_NEXT/Cataloging/web_staff_client_sprint2.txt b/docs/RELEASE_NOTES_NEXT/Cataloging/web_staff_client_sprint2.txt
index 8c4248b..63158d3 100644
--- a/docs/RELEASE_NOTES_NEXT/Cataloging/web_staff_client_sprint2.txt
+++ b/docs/RELEASE_NOTES_NEXT/Cataloging/web_staff_client_sprint2.txt
@@ -4,11 +4,15 @@ The web staff client now includes additional functionality
 to support cataloging and item maintenance, including:
 
 - a new MARC editor
+- the service backing the authority headings chooser now
+  has the ability to filter the browse by subject thesaurus
 - Z39.50 search and record import
 - improvements to copy and record bucket functionality
 - embedding the link checker interface
 - embedding the MARC batch import/export interface
-- the beginnings of the web staff volume/copy editor
+- the web staff volume/copy editor
 
-The web staff client remains a preview and is not recommended
-for production use.
+Nearly all of the cataloging functionality available in the XUL
+staff client is now present in the web staff client with the 
+exception of printing spine labels. Nonetheless, the web staff
+client remains a preview and is not recommended for production use.

commit 61b801d6b75fff05f78bae4b607b91eaec239ba8
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Fri Sep 11 13:40:30 2015 +0000

    webstaff: explicitly pass record type to MARC editor from Z39.50
    
    This fixes a bug where the Edit then Import action would
    pop up an empty MARC editor.
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/z3950/t_marc_edit.tt2 b/Open-ILS/src/templates/staff/cat/z3950/t_marc_edit.tt2
index 214aaa3..ab70dc9 100644
--- a/Open-ILS/src/templates/staff/cat/z3950/t_marc_edit.tt2
+++ b/Open-ILS/src/templates/staff/cat/z3950/t_marc_edit.tt2
@@ -5,7 +5,9 @@
     <h4 class="modal-title">[% l('Import Record') %]</h4>
   </div>
   <div class="modal-body">
-    <eg-marc-edit-record dirty-flag="dirty_flag" record-id="record_id" marc-xml="marc_xml"/>
+    <eg-marc-edit-record dirty-flag="dirty_flag" record-id="record_id" marc-xml="marc_xml"
+                         record-type="bre"
+    />
   </div>
   <div class="modal-footer">
     <button class="btn btn-warning" ng-click="cancel()">[% l('Cancel') %]</button>

commit e728e2eff65464f9591d44f292bd82ba601703e8
Author: Mike Rylander <mrylander at gmail.com>
Date:   Thu Sep 10 15:09:22 2015 -0400

    webstaff: Remove dependency-suggesting indent from "Use checkdigit" default
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/volcopy/t_defaults.tt2 b/Open-ILS/src/templates/staff/cat/volcopy/t_defaults.tt2
index 6c356c2..c11aa23 100644
--- a/Open-ILS/src/templates/staff/cat/volcopy/t_defaults.tt2
+++ b/Open-ILS/src/templates/staff/cat/volcopy/t_defaults.tt2
@@ -59,7 +59,7 @@
 
             <div class="row">
                 <div class="col-xs-12">
-                    <label style="padding-left: 25px">
+                    <label>
                         <input type="checkbox" ng-change="saveDefaults()" ng-model="defaults.barcode_checkdigit"/>
                         [% l('Use checkdigit') %]
                     </label>

commit 707d35fd7559b3a0d2d820d991c8f03678cd73e1
Author: Mike Rylander <mrylander at gmail.com>
Date:   Thu Sep 10 15:08:07 2015 -0400

    webstaff: Propagate changes to barcode auto-gen defaults immediately
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
index d337512..9d4d088 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
@@ -672,6 +672,14 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
     }
     $scope.fetchDefaults();
 
+    $scope.$watch('defaults.auto_gen_barcode', function (n,o) {
+        itemSvc.auto_gen_barcode = n
+    });
+
+    $scope.$watch('defaults.barcode_checkdigit', function (n,o) {
+        itemSvc.barcode_checkdigit = n
+    });
+
     $scope.dirty = false;
     $scope.$watch('dirty',
         function(newVal, oldVal) {

commit 183f9c30ac1fbb3a4c6895613f5b2e3214102f60
Author: Mike Rylander <mrylander at gmail.com>
Date:   Thu Sep 10 13:16:20 2015 -0400

    webstaff: Add barcode generation and checkdigit checking
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
index 86927a8..d337512 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
@@ -32,12 +32,60 @@ angular.module('egVolCopy',
 function(egCore , $q) {
 
     var service = {
+        currently_generating : false,
+        auto_gen_barcode : false,
+        barcode_checkdigit : false,
         new_cp_id : 0,
         new_cn_id : 0,
         tree : {}, // holds lib->cn->copy hash stack
         copies : [] // raw copy list
     };
 
+    service.nextBarcode = function(bc) {
+        service.currently_generating = true;
+        return egCore.net.request(
+            'open-ils.cat',
+            'open-ils.cat.item.barcode.autogen',
+            egCore.auth.token(),
+            bc, 1, { checkdigit: service.barcode_checkdigit }
+        ).then(function(resp) { // get_barcodes
+            var evt = egCore.evt.parse(resp);
+            if (!evt) return resp[0];
+            return '';
+        });
+    };
+
+    service.checkBarcode = function(bc) {
+        if (!service.barcode_checkdigit) return true;
+        if (bc != Number(bc)) return false;
+        bc = bc.toString();
+        // "16.00" == Number("16.00"), but the . is bad.
+        // Throw out any barcode that isn't just digits
+        if (bc.search(/\D/) != -1) return false;
+        var last_digit = bc.substr(bc.length-1);
+        var stripped_barcode = bc.substr(0,bc.length-1);
+        return service.barcodeCheckdigit(stripped_barcode).toString() == last_digit;
+    };
+
+    service.barcodeCheckdigit = function(bc) {
+        var reverse_barcode = bc.toString().split('').reverse();
+        var check_sum = 0; var multiplier = 2;
+        for (var i = 0; i < reverse_barcode.length; i++) {
+            var digit = reverse_barcode[i];
+            var product = digit * multiplier; product = product.toString();
+            var temp_sum = 0;
+            for (var j = 0; j < product.length; j++) {
+                temp_sum += Number( product[j] );
+            }
+            check_sum += Number( temp_sum );
+            multiplier = ( multiplier == 2 ? 1 : 2 );
+        }
+        check_sum = check_sum.toString();
+        var next_multiple_of_10 = (check_sum.match(/(\d*)\d$/)[1] * 10) + 10;
+        var check_digit = next_multiple_of_10 - Number(check_sum); if (check_digit == 10) check_digit = 0;
+        return check_digit;
+    };
+
     // returns a promise resolved with the list of circ mods
     service.get_classifications = function() {
         if (egCore.env.acnc)
@@ -193,7 +241,7 @@ function(egCore , $q) {
         replace: true,
         template:
             '<div class="row">'+
-                '<div class="col-xs-5">'+
+                '<div class="col-xs-5" ng-class="{'+"'has-error'"+':barcode_has_error}">'+
                     '<input id="{{callNumber.id()}}_{{copy.id()}}"'+
                     ' eg-enter="nextBarcode(copy.id())" class="form-control"'+
                     ' type="text" ng-model="barcode" ng-change="updateBarcode()"/>'+
@@ -206,12 +254,21 @@ function(egCore , $q) {
         controller : ['$scope','itemSvc','egCore',
             function ( $scope , itemSvc , egCore ) {
                 $scope.new_part_id = 0;
+                $scope.barcode_has_error = false;
 
                 $scope.nextBarcode = function (i) {
-                    $scope.focusNext(i);
+                    $scope.focusNext(i, $scope.barcode);
                 }
 
-                $scope.updateBarcode = function () { $scope.copy.barcode($scope.barcode); $scope.copy.ischanged(1); };
+                $scope.updateBarcode = function () {
+                    if ($scope.barcode != '')
+                        $scope.barcode_has_error = !Boolean(itemSvc.checkBarcode($scope.barcode));
+                    $scope.copy.barcode($scope.barcode);
+                    $scope.copy.ischanged(1);
+                    if (itemSvc.currently_generating)
+                        $scope.focusNext($scope.copy.id(), $scope.barcode);
+                };
+
                 $scope.updateCopyNo = function () { $scope.copy.copy_number($scope.copy_number); $scope.copy.ischanged(1); };
                 $scope.updatePart = function () {
                     if ($scope.part) {
@@ -289,7 +346,7 @@ function(egCore , $q) {
                 $scope.idTracker = function (x) { if (x && x.id) return x.id() };
 
                 // XXX $() is not working! arg
-                $scope.focusNextBarcode = function (i) {
+                $scope.focusNextBarcode = function (i, prev_bc) {
                     var n;
                     var yep = false;
                     angular.forEach($scope.copies, function (cp) {
@@ -306,9 +363,20 @@ function(egCore , $q) {
                     if (n) {
                         var next = '#' + $scope.callNumber.id() + '_' + n;
                         var el = $(next);
-                        if (el) el.focus()
+                        if (el) {
+                            if (!itemSvc.currently_generating) el.focus();
+                            if (prev_bc && itemSvc.auto_gen_barcode && el.val() == "") {
+                                itemSvc.nextBarcode(prev_bc).then(function(bc){
+                                    el.focus();
+                                    el.val(bc);
+                                    el.trigger('change');
+                                });
+                            } else {
+                                itemSvc.currently_generating = false;
+                            }
+                        }
                     } else {
-                        $scope.focusNext($scope.callNumber.id())
+                        $scope.focusNext($scope.callNumber.id(),prev_bc)
                     }
                 }
 
@@ -444,30 +512,37 @@ function(egCore , $q) {
                     }
                 });
 
-                $scope.focusNextFirst = function(prev_cn) {
+                $scope.focusNextFirst = function(prev_cn,prev_bc) {
                     var n;
                     var yep = false;
                     angular.forEach(Object.keys($scope.struct).sort(), function (cn) {
-                        console.log('checking '+cn);
                         if (n) return;
 
                         if (cn == prev_cn) {
-                            console.log('prev is '+cn);
                             yep = true;
                             return;
                         }
-                        console.log('prev is not '+cn);
 
                         if (yep) n = cn;
                     });
 
-                    console.log('found '+n);
                     if (n) {
                         var next = '#' + n + '_' + $scope.struct[n][0].id();
                         var el = $(next);
-                        if (el) el.focus()
+                        if (el) {
+                            if (!itemSvc.currently_generating) el.focus();
+                            if (prev_bc && itemSvc.auto_gen_barcode && el.val() == "") {
+                                itemSvc.nextBarcode(prev_bc).then(function(bc){
+                                    el.focus();
+                                    el.val(bc);
+                                    el.trigger('change');
+                                });
+                            } else {
+                                itemSvc.currently_generating = false;
+                            }
+                        }
                     } else {
-                        $scope.focusNext($scope.lib);
+                        $scope.focusNext($scope.lib, prev_bc);
                     }
                 }
 
@@ -527,7 +602,6 @@ function(egCore , $q) {
                                 .filter(function(x){ return parseInt(x) <= 0 });
                         for (var i = 0; i < how_many; i++) {
                             // Trimming the global list is a bit more tricky
-                            console.log('trying to trim ' + i);
                             angular.forEach($scope.struct[list[i]], function (d) {
                                 angular.forEach( $scope.allcopies, function (l, j) { 
                                     if (l === d) $scope.allcopies.splice(j,1);
@@ -551,6 +625,8 @@ function(egCore , $q) {
 function($scope , $q , $window , $routeParams , $location , $timeout , egCore , egNet , egGridDataProvider , itemSvc , $modal) {
 
     $scope.defaults = { // If defaults are not set at all, allow everything
+        barcode_checkdigit : false,
+        auto_gen_barcode : false,
         statcats : true,
         copy_notes : true,
         attributes : {
@@ -589,6 +665,8 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
                 $scope.batch.suffix = $scope.defaults.suffix;
                 $scope.working.statcat_filter = $scope.defaults.statcat_filter;
                 if ($scope.defaults.always_vols) $scope.show_vols = true;
+                if ($scope.defaults.barcode_checkdigit) itemSvc.barcode_checkdigit = true;
+                if ($scope.defaults.auto_gen_barcode) itemSvc.auto_gen_barcode = true;
             }
         });
     }
@@ -964,31 +1042,36 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
             }
         });
 
-        $scope.focusNextFirst = function(prev_lib) {
+        $scope.focusNextFirst = function(prev_lib,prev_bc) {
             var n;
             var yep = false;
             angular.forEach(Object.keys($scope.data.tree).sort(), function (lib) {
-                console.log('checking lib '+lib);
                 if (n) return;
 
                 if (lib == prev_lib) {
-                    console.log('prev is '+lib);
                     yep = true;
                     return;
                 }
-                console.log('prev is not '+lib);
 
                 if (yep) n = lib;
             });
 
-            console.log('found '+n);
             if (n) {
                 var first_cn = Object.keys($scope.data.tree[n])[0];
                 var next = '#' + first_cn + '_' + $scope.data.tree[n][first_cn][0].id();
                 var el = $(next);
-                if (el) el.focus()
-            } else {
-                $scope.focusNext($scope.lib);
+                if (el) {
+                    if (!itemSvc.currently_generating) el.focus();
+                    if (prev_bc && itemSvc.auto_gen_barcode && el.val() == "") {
+                        itemSvc.nextBarcode(prev_bc).then(function(bc){
+                            el.focus();
+                            el.val(bc);
+                            el.trigger('change');
+                        });
+                    } else {
+                        itemSvc.currently_generating = false;
+                    }
+                }
             }
         }
 
@@ -1292,6 +1375,8 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
             function ( $scope , $window , itemSvc , egCore ) {
 
                 $scope.defaults = { // If defaults are not set at all, allow everything
+                    barcode_checkdigit : false,
+                    auto_gen_barcode : false,
                     statcats : true,
                     copy_notes : true,
                     attributes : {

commit ff118a10373330832863df1eb4b77b9b729f9192
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Thu Sep 10 15:01:42 2015 +0000

    webstaff: tweak Phys Char Wizard layout
    
    Ensure that if a drop-down is particularly wide, it
    can no longer overlap with the previous/next buttons.
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/share/t_physchar_wizard.tt2 b/Open-ILS/src/templates/staff/cat/share/t_physchar_wizard.tt2
index f8d67d3..2b86979 100644
--- a/Open-ILS/src/templates/staff/cat/share/t_physchar_wizard.tt2
+++ b/Open-ILS/src/templates/staff/cat/share/t_physchar_wizard.tt2
@@ -26,7 +26,7 @@
     </div>
     <div ng-if="step != 0">
       <div class="col-md-4">{{label_for_step}}</div>
-      <div class="col-md-4">
+      <div class="col-md-6">
         <div class="btn-group" dropdown>
           <button type="button" class="btn btn-default dropdown-toggle">
            <span style="padding-right: 5px;" ng-if="selected_option">{{selected_option.label()}}</span>
@@ -43,11 +43,11 @@
         </div>
       </div>
     </div>
-    <div class="col-md-2">
+  </div>
+  <div class="row">
+    <div class="col-md-4 pull-right">
       <button class="btn btn-default" ng-click="prev_step()" 
         ng-class="{disabled : !step}">[% l('Previous') %]</button>
-    </div>
-    <div class="col-md-2">
       <button class="btn btn-default" ng-click="next_step()"
         ng-class="{disabled : is_last_step()}">[% l('Next') %]</button>
     </div>

commit e160112ac7a048ec525803ad49b10020c3a85590
Author: Bill Erickson <berickxx at gmail.com>
Date:   Wed Sep 9 20:52:14 2015 -0400

    webstaff: Phys Char Wiz various additions and repairs
    
     * Show <unset> as selector value when no value is selected
     * Properly handle gaps (undefined slots) in 007 field value
     * Return to origin value when dialog Cancel is chosen
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/share/t_physchar_wizard.tt2 b/Open-ILS/src/templates/staff/cat/share/t_physchar_wizard.tt2
index 1aa6576..f8d67d3 100644
--- a/Open-ILS/src/templates/staff/cat/share/t_physchar_wizard.tt2
+++ b/Open-ILS/src/templates/staff/cat/share/t_physchar_wizard.tt2
@@ -10,7 +10,8 @@
       <div class="col-md-4">
         <div class="btn-group" dropdown>
           <button type="button" class="btn btn-default dropdown-toggle">
-           <span style="padding-right: 5px;">{{selected_option.label()}}</span>
+           <span style="padding-right: 5px;" ng-if="selected_option">{{selected_option.label()}}</span>
+           <span style="padding-right: 5px;" ng-if="!selected_option">[% l('<unset>') | html %]</span>
            <span class="caret"></span>
          </button>
          <ul class="dropdown-menu">
@@ -28,7 +29,8 @@
       <div class="col-md-4">
         <div class="btn-group" dropdown>
           <button type="button" class="btn btn-default dropdown-toggle">
-           <span style="padding-right: 5px;">{{selected_option.label()}}</span>
+           <span style="padding-right: 5px;" ng-if="selected_option">{{selected_option.label()}}</span>
+           <span style="padding-right: 5px;" ng-if="!selected_option">[% l('<unset>') | html %]</span>
            <span class="caret"></span>
          </button>
          <ul class="dropdown-menu">
diff --git a/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js b/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
index 00b7901..26a1e6c 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
@@ -455,7 +455,8 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
                 $scope.spawnPhysCharWizard = function() {
                     var args = {
                         changed : false,
-                        field : $scope.field
+                        field : $scope.field,
+                        orig_value : $scope.field.data
                     };
                     $modal.open({
                         templateUrl: './cat/share/t_physchar_dialog',
@@ -464,11 +465,14 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
                             $scope.focusMe = true;
                             $scope.args = args;
                             $scope.ok = function(args) { $modalInstance.close(args) };
-                            $scope.cancel = function () { $modalInstance.dismiss() };
+                            $scope.cancel = function () { 
+                                $modalInstance.dismiss();
+                                args.field.data = args.orig_value;
+                            };
                         }],
                     }).result.then(function (args) {
-                        if (!args.changed) return;
-                        // $scope.field.data = ...
+                        // $scope.field.data is changed within the 
+                        // wizard.  Nothing left to do on submit.
                     });
 
                 }
@@ -1457,10 +1461,13 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
 
                 // $scope.step is the 1-based position in the list of 
                 // subfields for the currently selected type.
+                // step==0 means we are currently selecting the type
                 $scope.step = 0;
 
-                if (!$scope.field.data) $scope.field.data = '';
+                if (!$scope.field.data) 
+                    $scope.field.data = '';
 
+                // currently selected subfield value selector option
                 $scope.selected_option = null;
 
                 function current_ptype() {
@@ -1506,11 +1513,13 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
                     $scope.selected_option = option;
                     var new_val = option.value();
                     get_step_slot().then(function(slot) {
-                        // TODO fill in gaps with "|" values.
                         var value = $scope.field.data;
+                        while (value.length < (slot[0] + slot[1])) 
+                            value += ' ';
                         var before = value.substr(0, slot[0]);
                         var after = value.substr(slot[0] + slot[1]);
-                        $scope.field.data = before + new_val.substr(0, slot[1]) + after;
+                        $scope.field.data = 
+                            before + new_val.substr(0, slot[1]) + after;
                     });
                 }
 
@@ -1523,8 +1532,8 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
 
                 $scope.is_last_step = function() {
                     // This one is called w/ every digest, so avoid async
-                    // calls.  Wait until we know for sure if this is 
-                    // the last step.
+                    // calls.  Wait until we have loaded the current ptype
+                    // subfields to determine if this is the last step.
                     return (
                         current_ptype() && 
                         egTagTable.phys_char_sf_map[current_ptype()] &&
@@ -1563,14 +1572,14 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
                                 $scope.field.data, slot);
                             if (val) {
                                 $scope.selected_option = $scope.values_for_step
-                                .filter(function(opt) { return (opt.value() == val)})[0];
+                                .filter(function(opt) { 
+                                    return (opt.value() == val)})[0];
                             } else {
-                                $scope.selected_option = $scope.values_for_step[0];
+                                $scope.selected_option = null;
                             }
                         })
                     }
                 }
-
                 set_values_for_step();
             }
         ]

commit 96e6db3fa5d7fef0ff0bb2c6ca8d73e765104036
Author: Bill Erickson <berickxx at gmail.com>
Date:   Tue Sep 8 09:23:02 2015 -0400

    webstaff: Phys Char Wiz : wizard continued
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js b/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
index a624ccc..00b7901 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
@@ -1455,6 +1455,10 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
         controller: ['$scope','$q','egTagTable',
             function ($scope , $q , egTagTable) {
 
+                // $scope.step is the 1-based position in the list of 
+                // subfields for the currently selected type.
+                $scope.step = 0;
+
                 if (!$scope.field.data) $scope.field.data = '';
 
                 $scope.selected_option = null;
@@ -1465,34 +1469,7 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
 
                 function current_subfield() {
                     return egTagTable.getPhysCharSubfieldMap(current_ptype())
-                    .then(function(subfields) {
-                        // find the subfield at the current location
-                        return subfields.filter(function(sub) {
-                            return sub.start_pos() == $scope.step;
-                        })[0];
-                    });
-                }
-
-                function next_subfield() {
-                    return egTagTable.getPhysCharSubfieldMap(current_ptype())
-                    .then(function(sf_list) {
-                        for (var idx = 0; idx < sf_list.length; idx++) {
-                            if (sf_list[idx].start_pos() > $scope.step) {
-                                return sf_list[idx];
-                            }
-                        }
-                    });
-                }
-
-                function prev_subfield() {
-                    return egTagTable.getPhysCharSubfieldMap(current_ptype())
-                    .then(function(sf_list) {
-                        for (var idx = sf_list.length-1; idx >= 0; idx--) {
-                            if (sf_list[idx].start_pos() < $scope.step) {
-                                return sf_list[idx];
-                            }
-                        }
-                    });
+                    .then(function(sf_list) {return sf_list[$scope.step-1]});
                 }
 
                 $scope.values_for_step = [];
@@ -1528,9 +1505,13 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
                 $scope.change_option = function(option) {
                     $scope.selected_option = option;
                     var new_val = option.value();
-                    if (current_ptype() != new_val) {
-                        $scope.field.data = new_val; // total reset
-                    }
+                    get_step_slot().then(function(slot) {
+                        // TODO fill in gaps with "|" values.
+                        var value = $scope.field.data;
+                        var before = value.substr(0, slot[0]);
+                        var after = value.substr(slot[0] + slot[1]);
+                        $scope.field.data = before + new_val.substr(0, slot[1]) + after;
+                    });
                 }
 
                 function get_step_slot() {
@@ -1541,7 +1522,15 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
                 }
 
                 $scope.is_last_step = function() {
-                    return false; // TODO
+                    // This one is called w/ every digest, so avoid async
+                    // calls.  Wait until we know for sure if this is 
+                    // the last step.
+                    return (
+                        current_ptype() && 
+                        egTagTable.phys_char_sf_map[current_ptype()] &&
+                        egTagTable.phys_char_sf_map[current_ptype()].length 
+                            == $scope.step
+                    );
                 }
 
                 $scope.label_for_step = '';
@@ -1554,24 +1543,13 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
                 }
                 
                 $scope.next_step = function() {
-                    next_subfield().then(function(sf) {
-                        $scope.step = sf.start_pos()
-                        console.debug('setting step to ' + $scope.step);
-                        set_values_for_step();
-                    });
+                    $scope.step++;
+                    set_values_for_step();
                 }
 
                 $scope.prev_step = function() {
-                    if ($scope.step == 1) {
-                        $scope.step = 0;
-                        set_values_for_step();
-                    } else {
-                        prev_subfield().then(function(sf) {
-                            $scope.step = sf.start_pos();
-                            console.debug('setting step to ' + $scope.step);
-                            set_values_for_step();
-                        });
-                    }
+                    $scope.step--;
+                    set_values_for_step();
                 }
 
                 function set_selected_option_from_field() {
@@ -1593,7 +1571,6 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
                     }
                 }
 
-                $scope.step = 0; // always start with type selector
                 set_values_for_step();
             }
         ]

commit d656cb4083dabb6f85d49f4098bc649375c04a4f
Author: Bill Erickson <berickxx at gmail.com>
Date:   Mon Sep 7 20:37:40 2015 -0400

    webstaff: Phys Char Wiz : wizard continued
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/share/t_physchar_wizard.tt2 b/Open-ILS/src/templates/staff/cat/share/t_physchar_wizard.tt2
index 953ffe9..1aa6576 100644
--- a/Open-ILS/src/templates/staff/cat/share/t_physchar_wizard.tt2
+++ b/Open-ILS/src/templates/staff/cat/share/t_physchar_wizard.tt2
@@ -3,30 +3,51 @@
     <div class="col-md-4">[% l('007 Value') %]</div>
     <div class="col-md-4">{{field.data}}</div>
   </div>
+  <div class="row"><div class="col-md-12 pad-vert"><hr/></div></div>
   <div class="row">
-    <div ng-if="step == 'a'">
+    <div ng-if="step == 0">
       <div class="col-md-4">[% l('Category of Material') %]</div>
       <div class="col-md-4">
-        <select ng-model="value_for_step">
-          <option ng-repeat="option in values_for_step" 
-            value="{{option.ptype_key()}}">{{option.label()}}</option>
-        </select>
+        <div class="btn-group" dropdown>
+          <button type="button" class="btn btn-default dropdown-toggle">
+           <span style="padding-right: 5px;">{{selected_option.label()}}</span>
+           <span class="caret"></span>
+         </button>
+         <ul class="dropdown-menu">
+           <li ng-repeat="option in values_for_step">
+             <a href ng-click="change_ptype(option)">
+              {{option.label()}}
+             </a>
+           </li>
+         </ul>
+        </div>
       </div>
     </div>
-    <div ng-if="step != 'a'">
-      <div class="col-md-4" ng-show="step != 'a'">{{get_label_for_step()}}</div>
+    <div ng-if="step != 0">
+      <div class="col-md-4">{{label_for_step}}</div>
       <div class="col-md-4">
-        <select ng-model="value_for_step">
-          <option ng-repeat="option in values_for_step" 
-            value="{{option.value()}}">{{option.label()}}</option>
-        </select>
+        <div class="btn-group" dropdown>
+          <button type="button" class="btn btn-default dropdown-toggle">
+           <span style="padding-right: 5px;">{{selected_option.label()}}</span>
+           <span class="caret"></span>
+         </button>
+         <ul class="dropdown-menu">
+           <li ng-repeat="option in values_for_step">
+             <a href ng-click="change_option(option)">
+              {{option.label()}}
+             </a>
+           </li>
+         </ul>
+        </div>
       </div>
     </div>
     <div class="col-md-2">
-      <button class="btn btn-default">[% l('Previous') %]</button>
+      <button class="btn btn-default" ng-click="prev_step()" 
+        ng-class="{disabled : !step}">[% l('Previous') %]</button>
     </div>
     <div class="col-md-2">
-      <button class="btn btn-default">[% l('Next') %]</button>
+      <button class="btn btn-default" ng-click="next_step()"
+        ng-class="{disabled : is_last_step()}">[% l('Next') %]</button>
     </div>
   </div>
 </div>
diff --git a/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js b/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
index 4258173..a624ccc 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
@@ -1452,36 +1452,149 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
         scope : {
             field : '='
         },
-        controller: ['$scope','egTagTable',
-            function ($scope , egTagTable) {
-                $scope.step = 'a';
-                if ($scope.field.data) $scope.field.data = '';
+        controller: ['$scope','$q','egTagTable',
+            function ($scope , $q , egTagTable) {
 
-                $scope.value_for_step = '';
-                $scope.values_for_step = [];
+                if (!$scope.field.data) $scope.field.data = '';
 
-                egTagTable.getPhysCharTypeMap().then(function(list) {
-                    // we start with the types selector
-                    $scope.values_for_step = list;
-                });
+                $scope.selected_option = null;
 
-                $scope.current_ptype = function() {
+                function current_ptype() {
                     return $scope.field.data.substr(0, 1);   
                 }
 
-                $scope.get_label_for_step = function() {
-                    return 'TEST';
+                function current_subfield() {
+                    return egTagTable.getPhysCharSubfieldMap(current_ptype())
+                    .then(function(subfields) {
+                        // find the subfield at the current location
+                        return subfields.filter(function(sub) {
+                            return sub.start_pos() == $scope.step;
+                        })[0];
+                    });
+                }
+
+                function next_subfield() {
+                    return egTagTable.getPhysCharSubfieldMap(current_ptype())
+                    .then(function(sf_list) {
+                        for (var idx = 0; idx < sf_list.length; idx++) {
+                            if (sf_list[idx].start_pos() > $scope.step) {
+                                return sf_list[idx];
+                            }
+                        }
+                    });
+                }
+
+                function prev_subfield() {
+                    return egTagTable.getPhysCharSubfieldMap(current_ptype())
+                    .then(function(sf_list) {
+                        for (var idx = sf_list.length-1; idx >= 0; idx--) {
+                            if (sf_list[idx].start_pos() < $scope.step) {
+                                return sf_list[idx];
+                            }
+                        }
+                    });
                 }
 
-                $scope.get_values_for_step = function() {
-                    if ($scope.step == 'a') {
-                        egTagTable.getPhysCharTypeMap().then(function(list) {
-                            $scope.values_for_step = list;
+                $scope.values_for_step = [];
+                function set_values_for_step() {
+                    var promise;
+
+                    if ($scope.step == 0) {
+                        promise = egTagTable.getPhysCharTypeMap();
+                    } else {
+                        promise = current_subfield().then(
+                            function(subfield) {
+                                return egTagTable
+                                    .getPhysCharValueMap(subfield.id());
+                            }
+                        );
+                    }
+
+                    return promise.then(function(list) { 
+                        $scope.values_for_step = list;
+                        set_selected_option_from_field();
+                        set_label_for_step();
+                    });
+                }
+
+                $scope.change_ptype = function(option) {
+                    $scope.selected_option = option;
+                    var new_val = option.ptype_key();
+                    if (current_ptype() != new_val) {
+                        $scope.field.data = new_val; // total reset
+                    }
+                }
+
+                $scope.change_option = function(option) {
+                    $scope.selected_option = option;
+                    var new_val = option.value();
+                    if (current_ptype() != new_val) {
+                        $scope.field.data = new_val; // total reset
+                    }
+                }
+
+                function get_step_slot() {
+                    if ($scope.step == 0) return $q.when([0, 1]);
+                    return current_subfield().then(function(sf) {
+                        return [sf.start_pos(), sf.length()]
+                    });
+                }
+
+                $scope.is_last_step = function() {
+                    return false; // TODO
+                }
+
+                $scope.label_for_step = '';
+                function set_label_for_step() {
+                    if ($scope.step > 0) {
+                        current_subfield().then(function(sf) {
+                            $scope.label_for_step = sf.label();
+                        });
+                    }
+                }
+                
+                $scope.next_step = function() {
+                    next_subfield().then(function(sf) {
+                        $scope.step = sf.start_pos()
+                        console.debug('setting step to ' + $scope.step);
+                        set_values_for_step();
+                    });
+                }
+
+                $scope.prev_step = function() {
+                    if ($scope.step == 1) {
+                        $scope.step = 0;
+                        set_values_for_step();
+                    } else {
+                        prev_subfield().then(function(sf) {
+                            $scope.step = sf.start_pos();
+                            console.debug('setting step to ' + $scope.step);
+                            set_values_for_step();
                         });
+                    }
+                }
+
+                function set_selected_option_from_field() {
+                    if ($scope.step == 0) {
+                        $scope.selected_option = $scope.values_for_step
+                        .filter(function(opt) {
+                            return (opt.ptype_key() == current_ptype())})[0];
                     } else {
-                        //
+                        get_step_slot().then(function(slot) {
+                            var val = String.prototype.substr.apply(                      
+                                $scope.field.data, slot);
+                            if (val) {
+                                $scope.selected_option = $scope.values_for_step
+                                .filter(function(opt) { return (opt.value() == val)})[0];
+                            } else {
+                                $scope.selected_option = $scope.values_for_step[0];
+                            }
+                        })
                     }
                 }
+
+                $scope.step = 0; // always start with type selector
+                set_values_for_step();
             }
         ]
     }

commit 2b278e30e4c9dee5d9c1c58915bcff30abb6d5bd
Author: Bill Erickson <berickxx at gmail.com>
Date:   Mon Sep 7 13:46:01 2015 -0400

    webstaff: Phys Char Wiz : initial dialog / wizard
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/share/t_physchar_dialog.tt2 b/Open-ILS/src/templates/staff/cat/share/t_physchar_dialog.tt2
new file mode 100644
index 0000000..0070889
--- /dev/null
+++ b/Open-ILS/src/templates/staff/cat/share/t_physchar_dialog.tt2
@@ -0,0 +1,15 @@
+<div>
+  <div class="modal-header">
+    <button type="button" class="close"
+      ng-click="cancel()" aria-hidden="true">×</button>
+    <h4 class="modal-title">[% l('Physical Characteristics Wizard') %]</h4>
+  </div>
+  <div class="modal-body">
+    <eg-physchar-wizard changed="args.changed" field="args.field"/>
+  </div>
+  <div class="modal-footer">
+    <input type="submit" ng-click="ok(args)"
+        class="btn btn-primary" value="[% l('Save') %]"/>
+    <button class="btn btn-warning" ng-click="cancel()">[% l('Cancel') %]</button>
+  </div>
+</div>
diff --git a/Open-ILS/src/templates/staff/cat/share/t_physchar_wizard.tt2 b/Open-ILS/src/templates/staff/cat/share/t_physchar_wizard.tt2
new file mode 100644
index 0000000..953ffe9
--- /dev/null
+++ b/Open-ILS/src/templates/staff/cat/share/t_physchar_wizard.tt2
@@ -0,0 +1,32 @@
+<div>
+  <div class="row">
+    <div class="col-md-4">[% l('007 Value') %]</div>
+    <div class="col-md-4">{{field.data}}</div>
+  </div>
+  <div class="row">
+    <div ng-if="step == 'a'">
+      <div class="col-md-4">[% l('Category of Material') %]</div>
+      <div class="col-md-4">
+        <select ng-model="value_for_step">
+          <option ng-repeat="option in values_for_step" 
+            value="{{option.ptype_key()}}">{{option.label()}}</option>
+        </select>
+      </div>
+    </div>
+    <div ng-if="step != 'a'">
+      <div class="col-md-4" ng-show="step != 'a'">{{get_label_for_step()}}</div>
+      <div class="col-md-4">
+        <select ng-model="value_for_step">
+          <option ng-repeat="option in values_for_step" 
+            value="{{option.value()}}">{{option.label()}}</option>
+        </select>
+      </div>
+    </div>
+    <div class="col-md-2">
+      <button class="btn btn-default">[% l('Previous') %]</button>
+    </div>
+    <div class="col-md-2">
+      <button class="btn btn-default">[% l('Next') %]</button>
+    </div>
+  </div>
+</div>
diff --git a/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js b/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
index 40235c9..4258173 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
@@ -447,13 +447,30 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
                   '</div>',
         scope: { field: "=", onKeydown: '=' },
         controller : ['$scope','$modal',
-            function ( $scope,  $modal ) {
+            function ( $scope,  $modal) {
                 $scope.showPhysCharLink = function () {
                     return ($scope.$parent.$parent.record_type == 'bre') 
                         && $scope.field.tag == '007';
                 }
                 $scope.spawnPhysCharWizard = function() {
-                    console.log('HERE');
+                    var args = {
+                        changed : false,
+                        field : $scope.field
+                    };
+                    $modal.open({
+                        templateUrl: './cat/share/t_physchar_dialog',
+                        controller: ['$scope','$modalInstance',
+                            function( $scope , $modalInstance) {
+                            $scope.focusMe = true;
+                            $scope.args = args;
+                            $scope.ok = function(args) { $modalInstance.close(args) };
+                            $scope.cancel = function () { $modalInstance.dismiss() };
+                        }],
+                    }).result.then(function (args) {
+                        if (!args.changed) return;
+                        // $scope.field.data = ...
+                    });
+
                 }
             }
         ]
@@ -1427,6 +1444,50 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
     }
 })
 
+.directive("egPhyscharWizard", function () {
+    return {
+        restrict: 'E',
+        replace: true,
+        templateUrl: './cat/share/t_physchar_wizard',
+        scope : {
+            field : '='
+        },
+        controller: ['$scope','egTagTable',
+            function ($scope , egTagTable) {
+                $scope.step = 'a';
+                if ($scope.field.data) $scope.field.data = '';
+
+                $scope.value_for_step = '';
+                $scope.values_for_step = [];
+
+                egTagTable.getPhysCharTypeMap().then(function(list) {
+                    // we start with the types selector
+                    $scope.values_for_step = list;
+                });
+
+                $scope.current_ptype = function() {
+                    return $scope.field.data.substr(0, 1);   
+                }
+
+                $scope.get_label_for_step = function() {
+                    return 'TEST';
+                }
+
+                $scope.get_values_for_step = function() {
+                    if ($scope.step == 'a') {
+                        egTagTable.getPhysCharTypeMap().then(function(list) {
+                            $scope.values_for_step = list;
+                        });
+                    } else {
+                        //
+                    }
+                }
+            }
+        ]
+    }
+})
+
+
 .directive("egMarcEditAuthorityBrowser", function () {
     return {
         restrict: 'E',
diff --git a/Open-ILS/web/js/ui/default/staff/cat/services/tagtable.js b/Open-ILS/web/js/ui/default/staff/cat/services/tagtable.js
index de64a13..dcc92be 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/services/tagtable.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/services/tagtable.js
@@ -510,8 +510,8 @@ function($q,   egCore,   egAuth) {
             return $q.when(service.phys_char_type_map);
         }
 
-        return egCore.pcrud.retrieveAll('cmpctm')
-        .then(function(map) {service.phys_char_type_map = map});
+        return egCore.pcrud.retrieveAll('cmpctm', {}, {atomic : true})
+        .then(function(map) {return service.phys_char_type_map = map});
     }
 
     // Fetch+caches the config.marc21_physical_characteristic_subfield_map
@@ -525,9 +525,10 @@ function($q,   egCore,   egAuth) {
 
         return egCore.pcrud.search('cmpcsm', 
             {ptype_key : ptype_key},
-            {order_by : {cmpcsm : ['start_pos']}})
-        .then(function(maps) {
-            service.phys_char_sf_map[ptype_key] = maps;
+            {order_by : {cmpcsm : ['start_pos']}},
+            {atomic : true}
+        ).then(function(maps) {
+            return service.phys_char_sf_map[ptype_key] = maps;
         });
     }
 
@@ -541,9 +542,10 @@ function($q,   egCore,   egAuth) {
 
         return egCore.pcrud.search('cmpcvm', 
             {ptype_subfield : ptype_subfield},
-            {order_by : {cmpcsm : ['value']}})
-        .then(function(maps) {
-            service.phys_char_sf_map[ptype_subfield] = maps;
+            {order_by : {cmpcsm : ['value']}},
+            {atomic : true}
+        ).then(function(maps) {
+            return service.phys_char_sf_map[ptype_subfield] = maps;
         });
     }
 

commit a2d301fc77ebd55605c1157b058e1790057f447b
Author: Bill Erickson <berickxx at gmail.com>
Date:   Mon Sep 7 11:51:39 2015 -0400

    webstaff: Phys Char Wiz : launcher link
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js b/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
index d081f0c..40235c9 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
@@ -436,8 +436,27 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
                       'on-keydown="onKeydown" '+
                       'id="r{{field.record.subfield(\'901\',\'c\')[1]}}f{{field.position}}data"'+
                       '/></span>'+
+                      // TODO: move to TT2 template
+                      '<button class="btn btn-info btn-xs" '+
+                      'aria-label="Physical Characteristics Wizard" '+
+                      'ng-show="showPhysCharLink()"'+
+                      'ng-click="spawnPhysCharWizard()"'+
+                      '>'+
+                      '<span class="glyphicon glyphicon-link"></span>'+
+                      '</button>'+
                   '</div>',
-        scope: { field: "=", onKeydown: '=' }
+        scope: { field: "=", onKeydown: '=' },
+        controller : ['$scope','$modal',
+            function ( $scope,  $modal ) {
+                $scope.showPhysCharLink = function () {
+                    return ($scope.$parent.$parent.record_type == 'bre') 
+                        && $scope.field.tag == '007';
+                }
+                $scope.spawnPhysCharWizard = function() {
+                    console.log('HERE');
+                }
+            }
+        ]
     }
 })
 
diff --git a/Open-ILS/web/js/ui/default/staff/cat/services/tagtable.js b/Open-ILS/web/js/ui/default/staff/cat/services/tagtable.js
index 932794e..de64a13 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/services/tagtable.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/services/tagtable.js
@@ -534,7 +534,7 @@ function($q,   egCore,   egAuth) {
     // Fetches + caches the config.marc21_physical_characteristic_value_map
     // for the requested ptype_subfield (subfield_map.id).  
     // Maps are ordered by value.
-    serivice.getPhysCharValueMap = function(ptype_subfield) {
+    service.getPhysCharValueMap = function(ptype_subfield) {
         if (service.phys_char_value_map[ptype_subfield]) {
             return $q.when(service.phys_char_value_map[ptype_subfield]);
         }

commit da57d6c3ddd4c1992de290a7812771dec2be351b
Author: Bill Erickson <berickxx at gmail.com>
Date:   Mon Sep 7 11:23:48 2015 -0400

    webstaff: Phys Char Wiz : initial service funcs
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/cat/services/tagtable.js b/Open-ILS/web/js/ui/default/staff/cat/services/tagtable.js
index 6c3e10c..932794e 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/services/tagtable.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/services/tagtable.js
@@ -14,6 +14,9 @@ function($q,   egCore,   egAuth) {
         fields : { },
         ff_pos_map : { },
         ff_value_map : { },
+        phys_char_type_map : null,
+        phys_char_sf_map : { },
+        phys_char_value_map : { },
         authority_control_set : {
             _remote_loaded : false,
             _controlsets : [ ]
@@ -499,5 +502,50 @@ function($q,   egCore,   egAuth) {
         return service._active_control_set;
     }
 
+    // fetches and caches the full set of values from 
+    // config.marc21_physical_characteristic_type_map
+    service.getPhysCharTypeMap = function() {
+
+        if (service.phys_char_type_map) {
+            return $q.when(service.phys_char_type_map);
+        }
+
+        return egCore.pcrud.retrieveAll('cmpctm')
+        .then(function(map) {service.phys_char_type_map = map});
+    }
+
+    // Fetch+caches the config.marc21_physical_characteristic_subfield_map
+    // values for the requested ptype_key (i.e. type_map.ptype_key).
+    // Values are sorted by start_pos
+    service.getPhysCharSubfieldMap = function(ptype_key) {
+
+        if (service.phys_char_sf_map[ptype_key]) {
+            return $q.when(service.phys_char_sf_map[ptype_key]);
+        }
+
+        return egCore.pcrud.search('cmpcsm', 
+            {ptype_key : ptype_key},
+            {order_by : {cmpcsm : ['start_pos']}})
+        .then(function(maps) {
+            service.phys_char_sf_map[ptype_key] = maps;
+        });
+    }
+
+    // Fetches + caches the config.marc21_physical_characteristic_value_map
+    // for the requested ptype_subfield (subfield_map.id).  
+    // Maps are ordered by value.
+    serivice.getPhysCharValueMap = function(ptype_subfield) {
+        if (service.phys_char_value_map[ptype_subfield]) {
+            return $q.when(service.phys_char_value_map[ptype_subfield]);
+        }
+
+        return egCore.pcrud.search('cmpcvm', 
+            {ptype_subfield : ptype_subfield},
+            {order_by : {cmpcsm : ['value']}})
+        .then(function(maps) {
+            service.phys_char_sf_map[ptype_subfield] = maps;
+        });
+    }
+
     return service;
 }]);

commit 7d2d8cf109fbaa85242ac981e72319286ac06237
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Thu Sep 10 01:06:01 2015 +0000

    webstaff: implement Transfer Title Holds from record bucket
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/bucket/record/index.tt2 b/Open-ILS/src/templates/staff/cat/bucket/record/index.tt2
index 7b399c1..145ddd3 100644
--- a/Open-ILS/src/templates/staff/cat/bucket/record/index.tt2
+++ b/Open-ILS/src/templates/staff/cat/bucket/record/index.tt2
@@ -12,6 +12,8 @@
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/cat/services/record.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/cat/services/tagtable.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/cat/services/marcedit.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/services/holds.js"></script>
+[% INCLUDE 'staff/circ/share/hold_strings.tt2' %]
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/cat/bucket/record/app.js"></script>
 <script>
   angular.module('egCoreMod').run(['egStrings', function(s) {
diff --git a/Open-ILS/src/templates/staff/cat/bucket/record/t_view.tt2 b/Open-ILS/src/templates/staff/cat/bucket/record/t_view.tt2
index 599dab1..c1b2085 100644
--- a/Open-ILS/src/templates/staff/cat/bucket/record/t_view.tt2
+++ b/Open-ILS/src/templates/staff/cat/bucket/record/t_view.tt2
@@ -19,6 +19,9 @@
   <eg-grid-action label="[% l('Delete Selected Records from Catalog') %]" 
     handler="deleteRecordsFromCatalog"></eg-grid-action>
 
+  <eg-grid-action handler="transfer_holds_to_marked"
+    label="[% l('Transfer Title Holds') %]"></eg-grid-action>
+
   <eg-grid-action label="[% l('Merge Selected Records') %]" 
     handler="openRecordMergeDialog"></eg-grid-action>
 
diff --git a/Open-ILS/src/templates/staff/circ/share/hold_strings.tt2 b/Open-ILS/src/templates/staff/circ/share/hold_strings.tt2
index 9e67e19..647dac7 100644
--- a/Open-ILS/src/templates/staff/circ/share/hold_strings.tt2
+++ b/Open-ILS/src/templates/staff/circ/share/hold_strings.tt2
@@ -19,7 +19,9 @@ s.SET_TOP_OF_QUEUE =
 s.CLEAR_TOP_OF_QUEUE = 
   "[% l('Unset the Top of Queue flag for [_1] Hold(s)?', '{{num_holds}}') %]";
 s.TRANSFER_HOLD_TO_TITLE = 
-  "[% l('Tranfer [_1] Hold(s) to bib record ID [_2]?', '{{num_holds}}', '{{bib_id}}') %]";
+  "[% l('Transfer [_1] Hold(s) to bib record ID [_2]?', '{{num_holds}}', '{{bib_id}}') %]";
+s.TRANSFER_ALL_BIB_HOLDS_TO_TITLE = 
+  "[% l('Transfer holds on [_1] bib(s) to bib record ID [_2]?', '{{num_bibs}}', '{{bib_id}}') %]";
 s.NO_HOLD_TRANSFER_TITLE_MARKED = 
   "[% l('No record is marked as a hold transfer target!') %]";
 s.RETARGET_HOLDS = 
diff --git a/Open-ILS/web/js/ui/default/staff/cat/bucket/record/app.js b/Open-ILS/web/js/ui/default/staff/cat/bucket/record/app.js
index 98a73d6..f07bea7 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/bucket/record/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/bucket/record/app.js
@@ -503,10 +503,10 @@ function($scope,  $routeParams,  bucketSvc , egGridDataProvider) {
 }])
 
 .controller('ViewCtrl',
-       ['$scope','$q','$routeParams','bucketSvc', 'egCore', '$window',
-        '$timeout', 'egConfirmDialog', '$modal',
-function($scope,  $q , $routeParams,  bucketSvc, egCore, $window,
-        $timeout, egConfirmDialog, $modal) {
+       ['$scope','$q','$routeParams','bucketSvc','egCore','$window',
+        '$timeout','egConfirmDialog','$modal','egHolds',
+function($scope,  $q , $routeParams,  bucketSvc,  egCore,  $window,
+         $timeout,  egConfirmDialog,  $modal,  egHolds) {
 
     $scope.setTab('view');
     $scope.bucketId = $routeParams.id;
@@ -534,6 +534,12 @@ function($scope,  $q , $routeParams,  bucketSvc, egCore, $window,
         );
     }
 
+    // runs the transfer title holds action
+    $scope.transfer_holds_to_marked = function(records) {
+        var bib_ids = records.map(function(val) { return val.id; })
+        egHolds.transfer_all_bib_holds_to_marked_title(bib_ids);
+    }
+
     // opens the record merge dialog
     $scope.openRecordMergeDialog = function(records) {
         $modal.open({
diff --git a/Open-ILS/web/js/ui/default/staff/circ/services/holds.js b/Open-ILS/web/js/ui/default/staff/circ/services/holds.js
index 057cfa3..ea83238 100644
--- a/Open-ILS/web/js/ui/default/staff/circ/services/holds.js
+++ b/Open-ILS/web/js/ui/default/staff/circ/services/holds.js
@@ -368,6 +368,31 @@ function($modal , $q , egCore , egConfirmDialog , egAlertDialog) {
         });
     }
 
+    service.transfer_all_bib_holds_to_marked_title = function(bib_ids) {
+        if (!bib_ids.length) return $q.when();
+
+        var target_bib_id = egCore.hatch.getLocalItem(
+            'eg.circ.hold.title_transfer_target');
+
+        if (!target_bib_id) {
+            // no target marked
+            return egAlertDialog.open(
+                egCore.strings.NO_HOLD_TRANSFER_TITLE_MARKED).result;
+        }
+
+        return egConfirmDialog.open(
+            egCore.strings.TRANSFER_ALL_BIB_HOLDS_TO_TITLE, '', {
+                num_bibs : bib_ids.length,
+                bib_id : target_bib_id
+            }
+        ).result.then(function() {
+            return egCore.net.request(
+                'open-ils.circ',
+                'open-ils.circ.hold.change_title',
+                egCore.auth.token(), target_bib_id, bib_ids);
+        });
+    }
+
     // serially retargets each hold
     service.retarget = function(hold_ids) {
         if (!hold_ids.length) return $q.when();

commit a20da8af3762303f913b6f86baffe299cab00ed8
Author: Mike Rylander <mrylander at gmail.com>
Date:   Wed Sep 9 20:15:33 2015 -0400

    webstaff: Allow optional override of TITLE_LAST_COPY event
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/catalog/index.tt2 b/Open-ILS/src/templates/staff/cat/catalog/index.tt2
index f5db2fe..5abae22 100644
--- a/Open-ILS/src/templates/staff/cat/catalog/index.tt2
+++ b/Open-ILS/src/templates/staff/cat/catalog/index.tt2
@@ -26,6 +26,12 @@
       "[% l('Unlink selected conjoined copies?') %]";
     s.CONFIRM_DELETE_PEERS_MESSAGE =
       "[% l('Will unlink {{peers}} copies') %]";
+    s.CONFIRM_TRANSFER_COPIES_TO_MARKED_VOLUME =                                                                                                                  
+      "[% l('Are you sure you want to transfer selected items to the marked volume?') %]";                                                                                   
+    s.OVERRIDE_TRANSFER_COPIES_TO_MARKED_VOLUME_TITLE =                                                                                                           
+      "[% l('One or more items could not be transferred. Override?') %]";                                                                                                    
+    s.OVERRIDE_TRANSFER_COPIES_TO_MARKED_VOLUME_BODY =                                                                                                            
+      "[% l('Reason(s) include: [_1]', '{{evt_desc}}') %]";                
   }])
 </script>
 
diff --git a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
index 9639157..f60abf3 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
@@ -945,26 +945,46 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
 
     $scope.transferItems = function (){
         var xfer_target = egCore.hatch.getLocalItem('eg.cat.item_transfer_target');
-        if (xfer_target) {
-            var copy_list = gatherSelectedRawCopies();
-
-            angular.forEach(copy_list, function (cp) {
-                cp.call_number(xfer_target);
-            });
-
-            egCore.pcrud.update(
-                copy_list
-            ).then(function(success) {
-                if (success) {
-                    holdingsSvc.fetchAgain().then(function() {
-                        $scope.holdingsGridDataProvider.refresh();
-                    });
-                } else {
-                    alert('Could not transfer items!');
-                }
-            });
+        var copy_ids = gatherSelectedHoldingsIds();
+        if (xfer_target && copy_ids.length > 0) {
+            egCore.net.request(
+                'open-ils.cat',
+                'open-ils.cat.transfer_copies_to_volume',
+                egCore.auth.token(),
+                xfer_target,
+                copy_ids
+            ).then(
+                function(resp) { // oncomplete
+                    var evt = egCore.evt.parse(resp);
+                    if (evt) {
+                        egConfirmDialog.open(
+                            egCore.strings.OVERRIDE_TRANSFER_COPIES_TO_MARKED_VOLUME_TITLE,
+                            egCore.strings.OVERRIDE_TRANSFER_COPIES_TO_MARKED_VOLUME_BODY,
+                            {'evt_desc': evt.desc}
+                        ).result.then(function() {
+                            egCore.net.request(
+                                'open-ils.cat',
+                                'open-ils.cat.transfer_copies_to_volume.override',
+                                egCore.auth.token(),
+                                xfer_target,
+                                copy_ids,
+                                { events: ['TITLE_LAST_COPY', 'COPY_DELETE_WARNING'] }
+                            ).then(function(resp) {
+                                holdingsSvc.fetchAgain().then(function() {
+                                    $scope.holdingsGridDataProvider.refresh();
+                                });
+                            });
+                        });
+                    } else {
+                        holdingsSvc.fetchAgain().then(function() {
+                            $scope.holdingsGridDataProvider.refresh();
+                        });
+                    }
+                },
+                null, // onerror
+                null // onprogress
+            )
         }
-        
     }
 
     $scope.selectedHoldingsItemStatusTgrEvt = function (){

commit 9c6d3c2f9d0e559c806988d8013fd0dd98e39a22
Author: Jason Etheridge <jason at esilibrary.com>
Date:   Wed Sep 9 18:02:06 2015 -0400

    webstaff: Transfer Copies to Marked Volume
    
    in Copy Bucket
    
    Signed-off-by: Jason Etheridge <jason at esilibrary.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Cat.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Cat.pm
index 4a5d1b3..91566c2 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Cat.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Cat.pm
@@ -756,6 +756,76 @@ sub reset_hold_list {
     $ses->request('open-ils.circ.hold.reset.batch', $auth, $hold_ids);
 }
 
+__PACKAGE__->register_method(
+    method    => "transfer_copies_to_volume",
+    api_name  => "open-ils.cat.transfer_copies_to_volume",
+    argc      => 3,
+    signature => {
+        desc   => 'Transfers specified copies to the specified call number, and changes Circ Lib to match the new Owning Lib.',
+        params => [
+            {desc => 'Authtoken', type => 'string'},
+            {desc => 'Call Number ID', type => 'number'},
+            {desc => 'Array of Copy IDs', type => 'array'},
+        ]
+    },
+    return => {desc => '1 on success, Event on error'}
+);
+
+__PACKAGE__->register_method(
+    method   => "transfer_copies_to_volume",
+    api_name => "open-ils.cat.transfer_copies_to_volume.override",);
+
+sub transfer_copies_to_volume {
+    my( $self, $conn, $auth, $volume, $copies, $oargs ) = @_;
+    my $delete_stats = 1;
+    my $force_delete_empty_bib = undef;
+    my $create_parts = undef;
+
+    # initial tests
+
+    return 1 unless ref $copies;
+    my( $reqr, $evt ) = $U->checkses($auth);
+    return $evt if $evt;
+    my $editor = new_editor(requestor => $reqr, xact => 1);
+    if ($self->api_name =~ /override/) {
+        $oargs = { all => 1 } unless defined $oargs;
+    } else {
+        $oargs = {};
+    }
+
+    # does the volume exist?  good, we also need its owning_lib later
+    my( $cn, $cn_evt ) = $U->fetch_callnumber( $volume, 0, $editor );
+    return $cn_evt if $cn_evt;
+
+    # flesh and munge the copies
+    my $fleshed_copies = [];
+    my ($copy, $copy_evt);
+    foreach my $copy_id ( @{ $copies } ) {
+        ($copy, $copy_evt) = $U->fetch_copy($copy_id);
+        return $copy_evt if $copy_evt;
+        $copy->call_number( $volume );
+        $copy->circ_lib( $cn->owning_lib() );
+        $copy->ischanged( 't' );
+        push @$fleshed_copies, $copy;
+    }
+
+    # actual work
+    my $retarget_holds = [];
+    $evt = OpenILS::Application::Cat::AssetCommon->update_fleshed_copies(
+        $editor, $oargs, undef, $fleshed_copies, $delete_stats, $retarget_holds, $force_delete_empty_bib, $create_parts);
+
+    if( $evt ) { 
+        $logger->info("copy to volume transfer failed with event: ".OpenSRF::Utils::JSON->perl2JSON($evt));
+        $editor->rollback; 
+        return $evt; 
+    }
+
+    $editor->commit;
+    $logger->info("copy to volume transfer successfully updated ".scalar(@$copies)." copies");
+    reset_hold_list($auth, $retarget_holds);
+
+    return 1;
+}
 
 __PACKAGE__->register_method(
     method    => 'in_db_merge',
diff --git a/Open-ILS/src/templates/staff/cat/bucket/copy/index.tt2 b/Open-ILS/src/templates/staff/cat/bucket/copy/index.tt2
index db0e6dc..aa80153 100644
--- a/Open-ILS/src/templates/staff/cat/bucket/copy/index.tt2
+++ b/Open-ILS/src/templates/staff/cat/bucket/copy/index.tt2
@@ -17,6 +17,12 @@
       "[% l('One or more items could not be deleted. Override?') %]";
     s.OVERRIDE_DELETE_COPY_BUCKET_ITEMS_FROM_CATALOG_BODY =
       "[% l('Reason(s) include: [_1]', '{{evt_desc}}') %]";
+    s.CONFIRM_TRANSFER_COPY_BUCKET_ITEMS_TO_MARKED_VOLUME =
+      "[% l('Are you sure you want to transfer selected items to the marked volume?') %]";
+    s.OVERRIDE_TRANSFER_COPY_BUCKET_ITEMS_TO_MARKED_VOLUME_TITLE =
+      "[% l('One or more items could not be transferred. Override?') %]";
+    s.OVERRIDE_TRANSFER_COPY_BUCKET_ITEMS_TO_MARKED_VOLUME_BODY =
+      "[% l('Reason(s) include: [_1]', '{{evt_desc}}') %]";
   }])
 </script>
 [% END %]
diff --git a/Open-ILS/src/templates/staff/cat/bucket/copy/t_view.tt2 b/Open-ILS/src/templates/staff/cat/bucket/copy/t_view.tt2
index 420dd80..6a66ed6 100644
--- a/Open-ILS/src/templates/staff/cat/bucket/copy/t_view.tt2
+++ b/Open-ILS/src/templates/staff/cat/bucket/copy/t_view.tt2
@@ -16,6 +16,8 @@
     handler="spawnHoldingsEdit"></eg-grid-action>
   <eg-grid-action label="[% l('Remove Selected Copies') %]" 
     handler="detachCopies"></eg-grid-action>
+  <eg-grid-action label="[% l('Transfer Selected Copies to Marked Volume') %]" 
+    handler="transferCopies"></eg-grid-action>
   <eg-grid-action label="[% l('Delete Selected Copies from Catalog') %]" 
     handler="deleteCopiesFromCatalog"></eg-grid-action>
 
diff --git a/Open-ILS/web/js/ui/default/staff/cat/bucket/copy/app.js b/Open-ILS/web/js/ui/default/staff/cat/bucket/copy/app.js
index c866a65..ebc4bb9 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/bucket/copy/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/bucket/copy/app.js
@@ -602,6 +602,52 @@ function($scope,  $q , $routeParams , $timeout , $window , $modal , bucketSvc ,
         });
     }
 
+    $scope.transferCopies = function(copies) {
+        var xfer_target = egCore.hatch.getLocalItem('eg.cat.item_transfer_target');
+        var copy_ids = copies.map(
+            function(curr,idx,arr) {
+                return curr.id;
+            }
+        );
+        if (xfer_target) {
+            egCore.net.request(
+                'open-ils.cat',
+                'open-ils.cat.transfer_copies_to_volume',
+                egCore.auth.token(),
+                xfer_target,
+                copy_ids
+            ).then(
+                function(resp) { // oncomplete
+                    var evt = egCore.evt.parse(resp);
+                    if (evt) {
+                        egConfirmDialog.open(
+                            egCore.strings.OVERRIDE_TRANSFER_COPY_BUCKET_ITEMS_TO_MARKED_VOLUME_TITLE,
+                            egCore.strings.OVERRIDE_TRANSFER_COPY_BUCKET_ITEMS_TO_MARKED_VOLUME_BODY,
+                            {'evt_desc': evt.desc}
+                        ).result.then(function() {
+                            egCore.net.request(
+                                'open-ils.cat',
+                                'open-ils.cat.transfer_copies_to_volume.override',
+                                egCore.auth.token(),
+                                xfer_target,
+                                copy_ids,
+                                { events: ['TITLE_LAST_COPY', 'COPY_DELETE_WARNING'] }
+                            ).then(function(resp) {
+                                bucketSvc.bucketNeedsRefresh = true;
+                                drawBucket();
+                            });
+                        });
+                    } else {
+                        bucketSvc.bucketNeedsRefresh = true;
+                        drawBucket();
+                    }
+                },
+                null, // onerror
+                null // onprogress
+            )
+        }
+    }
+
     // fetch the bucket;  on error show the not-allowed message
     if ($scope.bucketId) 
         drawBucket()['catch'](function() { $scope.forbidden = true });

commit de538a7b15bc0736c66667115807fc7205cc79e5
Author: Mike Rylander <mrylander at gmail.com>
Date:   Wed Sep 9 19:51:27 2015 -0400

    webstaff: Allow editing of /only/ volume data
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2 b/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2
index d6805ad..14b46bd 100644
--- a/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2
@@ -1,5 +1,5 @@
 <div>
-    <div class="btn-group">
+    <div ng-show="!only_vols" class="btn-group">
         <label class="btn btn-default" ng-click="show_vols = !show_vols">
             <span ng-show="show_vols" style="padding-right: 5px;">[% l('Hide Volume/Copy Details') %]</span>
             <span ng-hide="show_vols" style="padding-right: 5px;">[% l('Show Volume/Copy Details') %]</span>
@@ -43,18 +43,30 @@
                     <div class="col-xs-1"><b>[% l('Prefix') %]</b></div>
                     <div class="col-xs-2"><b>[% l('Call Number') %]</b></div>
                     <div class="col-xs-1"><b>[% l('Suffix') %]</b></div>
-                    <div class="col-xs-1"><b>[% l('Copies') %]</b></div>
-                    <div class="col-xs-5">
+                    <div class="col-xs-1" ng-hide="only_vols"><b>[% l('Copies') %]</b></div>
+                    <div class="col-xs-5" ng-hide="only_vols">
                         <div class="row">
                             <div class="col-xs-5"><b>[% l('Barcode') %]</b></div>
                             <div class="col-xs-3"><b>[% l('Copy #') %]</b></div>
                             <div class="col-xs-4"><b>[% l('Part') %]</b></div>
                         </div>
                     </div>
+                    <div class="col-xs-2" ng-show="only_vols">
+                        <button class="btn btn-default center-block" ng-click="workingToComplete() && saveAndExit()" type="button">[% l('Save & Exit') %]</button>
+                    </div>
                 </div>
             </div>
         </div> <!-- row -->
-        <eg-vol-edit focus-next="focusNextFirst" ng-repeat="(lib,callnumbers) in data.tree | orderBy:lib track by lib" ng-init="ind = $index" record="record.id()" lib="{{lib}}" allcopies="data.copies" struct="data.tree[lib]"></eg-vol-edit>
+        <eg-vol-edit
+            focus-next="focusNextFirst"
+            ng-repeat="(lib,callnumbers) in data.tree | orderBy:lib track by lib"
+            ng-init="ind = $index"
+            record="record.id()"
+            only-vols="only_vols"
+            lib="{{lib}}"
+            allcopies="data.copies"
+            struct="data.tree[lib]">
+        </eg-vol-edit>
     </div>
 
 </div>
diff --git a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
index fde2e06..86927a8 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
@@ -275,13 +275,13 @@ function(egCore , $q) {
                 '<div class="col-xs-1">'+
                     '<select class="form-control" ng-model="suffix" ng-change="updateSuffix()" ng-options="s.label() for s in suffix_list track by idTracker(s)"/>'+
                 '</div>'+
-                '<div class="col-xs-1"><input class="form-control" type="number" ng-model="copy_count" min="{{orig_copy_count}}" ng-change="changeCPCount()"></div>'+
-                '<div class="col-xs-5">'+
+                '<div ng-hide="onlyVols" class="col-xs-1"><input class="form-control" type="number" ng-model="copy_count" min="{{orig_copy_count}}" ng-change="changeCPCount()"></div>'+
+                '<div ng-hide="onlyVols" class="col-xs-5">'+
                     '<eg-vol-copy-edit ng-repeat="cp in copies track by idTracker(cp)" focus-next="focusNextBarcode" copy="cp" call-number="callNumber"></eg-vol-copy-edit>'+
                 '</div>'+
             '</div>',
 
-        scope: {focusNext: "=", allcopies: "=", copies: "=" },
+        scope: {focusNext: "=", allcopies: "=", copies: "=", onlyVols: "=" },
         controller : ['$scope','itemSvc','egCore',
             function ( $scope , itemSvc , egCore ) {
                 $scope.callNumber =  $scope.copies[0].call_number();
@@ -424,14 +424,14 @@ function(egCore , $q) {
                 '<div class="col-xs-1"><eg-org-selector selected="owning_lib" disableTest="cant_have_vols"></eg-org-selector></div>'+
                 '<div class="col-xs-1"><input class="form-control" type="number" min="{{orig_cn_count}}" ng-model="cn_count" ng-change="changeCNCount()"/></div>'+
                 '<div class="col-xs-10">'+
-                    '<eg-vol-row '+
+                    '<eg-vol-row only-vols="onlyVols"'+
                         'ng-repeat="(cn,copies) in struct | orderBy:cn track by cn" '+
                         'focus-next="focusNextFirst" copies="copies" allcopies="allcopies">'+
                     '</eg-vol-row>'+
                 '</div>'+
             '</div>',
 
-        scope: { focusNext: "=", allcopies: "=", struct: "=", lib: "@", record: "@" },
+        scope: { focusNext: "=", allcopies: "=", struct: "=", lib: "@", record: "@", onlyVols: "=" },
         controller : ['$scope','itemSvc','egCore',
             function ( $scope , itemSvc , egCore ) {
                 $scope.first_cn = Object.keys($scope.struct)[0];
@@ -607,6 +607,7 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
         }
     );
 
+    $scope.only_vols = false;
     $scope.show_vols = true;
     $scope.show_copies = true;
 
@@ -629,6 +630,8 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
                     $scope.completed_copies = $scope.completed_copies.concat(itemSvc.copies.splice(i,1));
             });
         });
+
+        return true;
     }
 
     $scope.completeToWorking = function () {
@@ -638,6 +641,8 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
                     itemSvc.copies = itemSvc.copies.concat($scope.completed_copies.splice(i,1));
             });
         });
+
+        return true;
     }
 
     createSimpleUpdateWatcher = function (field) {
@@ -850,7 +855,10 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
 
             if (data) {
                 if (data.hide_vols && !$scope.defaults.always_vols) $scope.show_vols = false;
-                if (data.hide_copies) $scope.show_copies = false;
+                if (data.hide_copies) {
+                    $scope.show_copies = false;
+                    $scope.only_vols = true;
+                }
 
                 $scope.record_id = data.record_id;
 

commit cb532decc7af7a88d15453fcef7d7d0ab6a31c4c
Author: Mike Rylander <mrylander at gmail.com>
Date:   Wed Sep 9 19:51:06 2015 -0400

    webstaff: Allow adding vol/copy to an empty bib (or when no vol/lib is selected)
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
index 0fb437e..9639157 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
@@ -840,6 +840,8 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
             );
         }
 
+        if (raw.length == 0) raw.push({});
+
         egCore.net.request(
             'open-ils.actor',
             'open-ils.actor.anon_cache.set_value',

commit 4d82cd7a9f306855008b0b6e3d1797c75d340d90
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Wed Sep 9 22:56:08 2015 +0000

    webstaff: implement Merge Selected Records from record bucket
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/bucket/record/index.tt2 b/Open-ILS/src/templates/staff/cat/bucket/record/index.tt2
index 2c45eff..7b399c1 100644
--- a/Open-ILS/src/templates/staff/cat/bucket/record/index.tt2
+++ b/Open-ILS/src/templates/staff/cat/bucket/record/index.tt2
@@ -8,6 +8,10 @@
 [% BLOCK APP_JS %]
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/services/grid.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/services/ui.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/marcrecord.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/cat/services/record.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/cat/services/tagtable.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/cat/services/marcedit.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/cat/bucket/record/app.js"></script>
 <script>
   angular.module('egCoreMod').run(['egStrings', function(s) {
diff --git a/Open-ILS/src/templates/staff/cat/bucket/record/t_edit_lead_record.tt2 b/Open-ILS/src/templates/staff/cat/bucket/record/t_edit_lead_record.tt2
new file mode 100644
index 0000000..60f3a09
--- /dev/null
+++ b/Open-ILS/src/templates/staff/cat/bucket/record/t_edit_lead_record.tt2
@@ -0,0 +1,16 @@
+<div>
+  <div class="modal-header">
+    <button type="button" class="close"
+      ng-click="cancel()" aria-hidden="true">×</button>
+    <h4 class="modal-title">[% l('Edit Lead Record') %]</h4>
+  </div>
+  <div class="modal-body">
+    <eg-marc-edit-record dirty-flag="dirty_flag" record-id="record_id"
+                         record-type="bre" />
+  </div>
+  <div class="modal-footer">
+    <input type="submit" ng-click="ok()"
+        class="btn btn-primary" value="[% l('Done') %]"/>
+    <button class="btn btn-warning" ng-click="cancel()">[% l('Cancel') %]</button>
+  </div>
+</div>
diff --git a/Open-ILS/src/templates/staff/cat/bucket/record/t_merge_records.tt2 b/Open-ILS/src/templates/staff/cat/bucket/record/t_merge_records.tt2
new file mode 100644
index 0000000..e25220a
--- /dev/null
+++ b/Open-ILS/src/templates/staff/cat/bucket/record/t_merge_records.tt2
@@ -0,0 +1,40 @@
+<div>
+  <div class="modal-header">
+    <button type="button" class="close"
+      ng-click="cancel()" aria-hidden="true">×</button>
+    <h4 class="modal-title">[% l('Merge records?') %]</h4>
+  </div>
+  <div class="modal-body">
+      <div class="row">
+          <div class="col-xs-6">
+            <h4>[% l('Lead record') %]</h4>
+            <div ng-if="lead_id">
+               <button class="btn btn-default btn-sm" ng-click="edit_lead()">[% l('Edit') %]</button>
+               <eg-record-html record-id="lead_id"></eg-record-html>
+            </div>
+            <div ng-if="!lead_id">
+                [% l('Please select a lead record from the right...') %]
+            </div>
+          </div>
+          <div class="col-xs-6">
+            <h4>[% l('Records to merge into lead') %]</h4>
+            <accordion>
+              <accordion-group ng-repeat="rec in records">
+                <accordion-heading>
+                    [% l('Record [_1]', '{{rec.id}}') %] <i class="pull-right glyphicon" ng-class="{'glyphicon-chevron-down': status.open, 'glyphicon-chevron-right': !status.open}"></i>
+                </accordion-heading>
+                <button class="btn btn-default btn-sm" ng-click="use_as_lead(rec)">[% l('Use as lead record') %]</button>
+                <button class="btn btn-default btn-sm" ng-click="drop(rec)">[% l('Remove from consideration') %]</button>
+                <eg-record-html record-id="rec.id"></eg-record-html>
+              </accordion-group>
+            </accordian>
+          </div>
+      </div>
+  </div>
+  <div class="modal-footer">
+    <input type="submit" ng-click="ok()"
+        ng-class="{disabled : !lead_id || records.length < 1 }"
+        class="btn btn-primary" value="[% l('Merge') %]"/>
+    <button class="btn btn-warning" ng-click="cancel()">[% l('Cancel') %]</button>
+  </div>
+</div>
diff --git a/Open-ILS/src/templates/staff/cat/bucket/record/t_view.tt2 b/Open-ILS/src/templates/staff/cat/bucket/record/t_view.tt2
index 9ff16bf..599dab1 100644
--- a/Open-ILS/src/templates/staff/cat/bucket/record/t_view.tt2
+++ b/Open-ILS/src/templates/staff/cat/bucket/record/t_view.tt2
@@ -19,6 +19,9 @@
   <eg-grid-action label="[% l('Delete Selected Records from Catalog') %]" 
     handler="deleteRecordsFromCatalog"></eg-grid-action>
 
+  <eg-grid-action label="[% l('Merge Selected Records') %]" 
+    handler="openRecordMergeDialog"></eg-grid-action>
+
   <eg-grid-action label="[% l('Export Records') %]" 
     handler="openExportBucketDialog"></eg-grid-action>
 
diff --git a/Open-ILS/web/js/ui/default/staff/cat/bucket/record/app.js b/Open-ILS/web/js/ui/default/staff/cat/bucket/record/app.js
index 4f780fd..98a73d6 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/bucket/record/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/bucket/record/app.js
@@ -13,7 +13,7 @@
  */
 
 angular.module('egCatRecordBuckets', 
-    ['ngRoute', 'ui.bootstrap', 'egCoreMod', 'egUiMod', 'egGridMod'])
+    ['ngRoute', 'ui.bootstrap', 'egCoreMod', 'egUiMod', 'egGridMod', 'egMarcMod'])
 
 .config(function($routeProvider, $locationProvider, $compileProvider) {
     $locationProvider.html5Mode(true);
@@ -534,6 +534,73 @@ function($scope,  $q , $routeParams,  bucketSvc, egCore, $window,
         );
     }
 
+    // opens the record merge dialog
+    $scope.openRecordMergeDialog = function(records) {
+        $modal.open({
+            templateUrl: './cat/bucket/record/t_merge_records',
+            size: 'lg',
+            controller:
+                ['$scope', '$modalInstance', function($scope, $modalInstance) {
+                $scope.records = [];
+                $scope.lead_id = 0;
+                angular.forEach(records, function(rec) {
+                    $scope.records.push({ id : rec.id });
+                });
+                $scope.ok = function() {
+                    $modalInstance.close({
+                        lead_id : $scope.lead_id,
+                        records : $scope.records
+                    });
+                }
+                $scope.cancel = function () { $modalInstance.dismiss() }
+                $scope.use_as_lead = function(rec) {
+                    if ($scope.lead_id) {
+                        $scope.records.push({ id : $scope.lead_id });
+                    }
+                    $scope.lead_id = rec.id;
+                    $scope.drop(rec);
+                }
+                $scope.drop = function(rec) {
+                    angular.forEach($scope.records, function(val, i) {
+                        if (rec == $scope.records[i]) {
+                            $scope.records.splice(i, 1);
+                        }
+                    });
+                }
+                $scope.edit_lead = function() {
+                    var lead_id = $scope.lead_id;
+                    $modal.open({
+                        templateUrl: './cat/bucket/record/t_edit_lead_record',
+                        size: 'lg',
+                        controller:
+                            ['$scope', '$modalInstance', function($scope, $modalInstance) {
+                            $scope.focusMe = true;
+                            $scope.record_id = lead_id;
+                            $scope.dirty_flag = false;
+                            $scope.ok = function() { $modalInstance.close() }
+                            $scope.cancel = function () { $modalInstance.dismiss() }
+                        }]
+                    }).result.then(function() {
+                        // TODO: need a way to force a refresh of the egRecordHtml, as
+                        // the record ID does not change
+                    });
+                };
+            }]
+        }).result.then(function (args) {
+            if (!args.lead_id) return;
+            if (!args.records.length) return;
+            egCore.net.request(
+                'open-ils.cat',
+                'open-ils.cat.biblio.records.merge',
+                egCore.auth.token(),
+                args.lead_id,
+                args.records.map(function(val) { return val.id; })
+            ).then(function() {
+                drawBucket();
+            });
+        });
+    }
+
     $scope.showAllRecords = function() {
         // TODO: maybe show selected would be better?
         // TODO: probably want to set a limit on the number of

commit bd9a659e3c9ded9b8880cf68bc817eca2e749997
Author: Mike Rylander <mrylander at gmail.com>
Date:   Wed Sep 9 16:26:44 2015 -0400

    webstaff: Add "add items to bucket" action to holdings maint
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/catalog/t_add_to_bucket.tt2 b/Open-ILS/src/templates/staff/cat/catalog/t_add_to_bucket.tt2
index eb53d04..cbcf35d 100644
--- a/Open-ILS/src/templates/staff/cat/catalog/t_add_to_bucket.tt2
+++ b/Open-ILS/src/templates/staff/cat/catalog/t_add_to_bucket.tt2
@@ -5,24 +5,29 @@
 </div>
 <div class="modal-body">
   <div class="row">
-    <div class="form-inline">
-      <div class="form-gruop">
-        <label for="select-bucket">[% l('Copy this record into which bucket?') %]</label>
-        <select id="select-bucket" class="form-control" ng-model="bucket_id"
-                ng-options="bucket.name() for bucket in allBuckets track by bucket.id()">
-        </select>
-        <button class="btn btn-primary" ng-class="{disabled : !bucket_id}" 
-                ng-click="add_to_bucket()">[% l('Add To Selected Bucket') %]</button>
-      </div>
+    <div class="col-md-4">
+      <label for="select-bucket">[% l('Name of existing bucket') %]</label>
+    </div>
+    <div class="col-md-4">
+      <select id="select-bucket" class="form-control" ng-model="bucket_id"
+              ng-options="bucket.id() as bucket.name() for bucket in allBuckets | orderBy:bucket.name()">
+      </select>
+    </div>
+    <div class="col-md-4">
+      <button class="btn btn-primary" ng-class="{disabled : !bucket_id}" 
+          ng-click="add_to_bucket()">[% l('Add To Selected Bucket') %]</button>
     </div>
   </div>
-  <div class="row">
-    <div class="form-inline">
-      <div class="form-gruop">
-        <label for="new-bucket-name">[% l('Name For New Bucket') %]</label>
+  <div class="row pad-vert">
+    <div class="col-md-4">
+      <label for="new-bucket-name">[% l('Name For New Bucket') %]</label>
+    </div>
+    <div class="col-md-4">
         <input type="text" class="form-control" id="new-bucket-name" ng-model="newBucketName" />
-        <button class="btn btn-primary" ng-class="{disabled : !newBucketName}"
-                ng-click="add_to_new_bucket()">[% l('Add To New Bucket') %]</button>
+    </div>
+    <div class="col-md-4">
+      <button class="btn btn-primary" ng-class="{disabled : !newBucketName}"
+          ng-click="add_to_new_bucket()">[% l('Add To New Bucket') %]</button>
       </div>
   </div>
   </div>
diff --git a/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2 b/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2
index 6b79312..84783e8 100644
--- a/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2
+++ b/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2
@@ -33,6 +33,8 @@
       checkbox="holdings_show_vols"
       checked="holdings_show_vols"/>
 
+    <eg-grid-action handler="add_copies_to_bucket"
+      label="[% l('Add Items to Bucket') %]"></eg-grid-action>
     <eg-grid-action handler="requestItems"
       label="[% l('Request Items') %]"></eg-grid-action>
     <eg-grid-action handler="attach_to_peer_bib"
diff --git a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
index 8b84e23..0fb437e 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
@@ -462,6 +462,73 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
         }
     });
 
+    $scope.add_copies_to_bucket = function() {
+        var copy_list = gatherSelectedHoldingsIds();
+        if (copy_list.length == 0) return;
+
+        return $modal.open({
+            templateUrl: './cat/catalog/t_add_to_bucket',
+            animation: true,
+            size: 'md',
+            controller:
+                   ['$scope','$modalInstance',
+            function($scope , $modalInstance) {
+
+                $scope.bucket_id = 0;
+                $scope.newBucketName = '';
+                $scope.allBuckets = [];
+
+                egCore.net.request(
+                    'open-ils.actor',
+                    'open-ils.actor.container.retrieve_by_class.authoritative',
+                    egCore.auth.token(), egCore.auth.user().id(),
+                    'copy', 'staff_client'
+                ).then(function(buckets) { $scope.allBuckets = buckets; });
+
+                $scope.add_to_bucket = function() {
+                    var promises = [];
+                    angular.forEach(copy_list, function (cp) {
+                        var item = new egCore.idl.ccbi()
+                        item.bucket($scope.bucket_id);
+                        item.target_copy(cp);
+                        promises.push(
+                            egCore.net.request(
+                                'open-ils.actor',
+                                'open-ils.actor.container.item.create',
+                                egCore.auth.token(), 'copy', item
+                            )
+                        );
+
+                        return $q.all(promises).then(function() {
+                            $modalInstance.close();
+                        });
+                    });
+                }
+
+                $scope.add_to_new_bucket = function() {
+                    var bucket = new egCore.idl.ccb();
+                    bucket.owner(egCore.auth.user().id());
+                    bucket.name($scope.newBucketName);
+                    bucket.description('');
+                    bucket.btype('staff_client');
+
+                    return egCore.net.request(
+                        'open-ils.actor',
+                        'open-ils.actor.container.create',
+                        egCore.auth.token(), 'copy', bucket
+                    ).then(function(bucket) {
+                        $scope.bucket_id = bucket;
+                        $scope.add_to_bucket();
+                    });
+                }
+
+                $scope.cancel = function() {
+                    $modalInstance.dismiss();
+                }
+            }]
+        });
+    }
+
     $scope.requestItems = function() {
         var copy_list = gatherSelectedHoldingsIds();
         if (copy_list.length == 0) return;

commit 68eb69e80578ad94699739dc74ebf1748a98658c
Author: Mike Rylander <mrylander at gmail.com>
Date:   Wed Sep 9 13:57:41 2015 -0400

    webstaff: Refactor replace-barcode to be reusable, and reuse it in holdings maint
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2 b/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2
index c7a1142..6b79312 100644
--- a/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2
+++ b/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2
@@ -67,6 +67,8 @@
       label="[% l('Copies') %]"></eg-grid-action>
     <eg-grid-action handler="selectedHoldingsVolCopyEdit" group="[% l('Edit') %]"
       label="[% l('Volumes and Copies') %]"></eg-grid-action>
+    <eg-grid-action handler="replaceBarcodes" group="[% l('Edit') %]"
+      label="[% l('Replace Barcodes') %]"></eg-grid-action>
 
     <eg-grid-action handler="selectedHoldingsEmptyVolCopyDelete" group="[% l('Delete') %]" disabled="vols_not_shown"
       label="[% l('Empty Volumes') %]"></eg-grid-action>
diff --git a/Open-ILS/src/templates/staff/cat/item/replace_barcode/index.tt2 b/Open-ILS/src/templates/staff/cat/item/replace_barcode/index.tt2
index 6472a48..a81f7cd 100644
--- a/Open-ILS/src/templates/staff/cat/item/replace_barcode/index.tt2
+++ b/Open-ILS/src/templates/staff/cat/item/replace_barcode/index.tt2
@@ -10,40 +10,6 @@
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/cat/item/replace_barcode/app.js"></script>
 [% END %]
 
-<h2>[% l('Replace Item Barcode') %]</h2>
+[% INCLUDE 'staff/cat/share/t_replace_barcode.tt2' %]
 
-<div class="row">
-  <div class="col-md-6 pad-vert">
-    <form role="form" ng-submit="updateBarcode()">
-      <div class="form-group">
-        <label for="barcode1">[% l('Enter Original Barcode for Item') %]</label>
-        <input type="text" class="form-control" id="barcode1" required
-          ng-model="barcode1"
-          placeholder="[% l('Original Barcode...') %]" select-me="focusBarcode">
-      </div>
-      <div class="form-group">
-        <label for="barcode2">[% l('Enter New Barcode for Item') %]</label>
-        <input type="text" class="form-control" id="barcode2" 
-          ng-model="barcode2"
-          required placeholder="[% l('New Barcode...') %]">
-      </div>
-      <button type="submit" class="btn btn-default">[% l('Submit') %]</button>
-    </form>
-  </div>
-</div>
-
-<div class="row pad-vert">
-  <div class="col-md-6">
-    <div class="alert alert-danger" ng-if="copyNotFound">
-      [% l('Copy Not Found') %]
-    </div>
-    <div class="alert alert-success" ng-if="updateOK">
-      <span>[% l('Copy Updated') %]</span>
-      <span class="horiz-pad" ng-if="copyId">
-        <a href="./cat/item/{{copyId}}/summary" target="_self">
-          [% l('View Item Details') %]
-        </a>
-    </div>
-  </div>
-</div>
 [% END %]
diff --git a/Open-ILS/src/templates/staff/cat/item/replace_barcode/index.tt2 b/Open-ILS/src/templates/staff/cat/share/t_replace_barcode.tt2
similarity index 67%
copy from Open-ILS/src/templates/staff/cat/item/replace_barcode/index.tt2
copy to Open-ILS/src/templates/staff/cat/share/t_replace_barcode.tt2
index 6472a48..83702a3 100644
--- a/Open-ILS/src/templates/staff/cat/item/replace_barcode/index.tt2
+++ b/Open-ILS/src/templates/staff/cat/share/t_replace_barcode.tt2
@@ -1,19 +1,14 @@
-[%
-  WRAPPER "staff/base.tt2";
-  ctx.page_title = l("Replace Item Barcode"); 
-  ctx.page_app = "egItemReplaceBarcode";
-  ctx.page_ctrl = "ReplaceItemBarcodeCtrl";
-%]
-
-[% BLOCK APP_JS %]
-<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/ui.js"></script>
-<script src="[% ctx.media_prefix %]/js/ui/default/staff/cat/item/replace_barcode/app.js"></script>
-[% END %]
-
-<h2>[% l('Replace Item Barcode') %]</h2>
+<div>
+<div class="row ">
+  <div class="col-md-1"></div>
+  <div class="col-md-6">
+    <h2 class="center-block">[% l('Replace Item Barcode') %]</h2>
+  </div>
+</div>
 
-<div class="row">
-  <div class="col-md-6 pad-vert">
+<div class="row pad-vert">
+  <div class="col-md-1"></div>
+  <div ng-class="{'col-md-6': !isModal, 'col-md-10': isModal}">
     <form role="form" ng-submit="updateBarcode()">
       <div class="form-group">
         <label for="barcode1">[% l('Enter Original Barcode for Item') %]</label>
@@ -23,11 +18,12 @@
       </div>
       <div class="form-group">
         <label for="barcode2">[% l('Enter New Barcode for Item') %]</label>
-        <input type="text" class="form-control" id="barcode2" 
+        <input type="text" class="form-control" id="barcode2" select-me="focusBarcode2"
           ng-model="barcode2"
           required placeholder="[% l('New Barcode...') %]">
       </div>
       <button type="submit" class="btn btn-default">[% l('Submit') %]</button>
+      <button ng-if="isModal" class="btn btn-warning" ng-click="cancel($event)">[% l('Cancel') %]</button>
     </form>
   </div>
 </div>
@@ -46,4 +42,4 @@
     </div>
   </div>
 </div>
-[% END %]
+</div>
diff --git a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
index 20fe682..8b84e23 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
@@ -531,6 +531,63 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
         });
     }
 
+    $scope.replaceBarcodes = function() {
+        var copy_list = gatherSelectedRawCopies();
+        if (copy_list.length == 0) return;
+
+        var holdingsGridDataProviderRef = $scope.holdingsGridDataProvider;
+
+        angular.forEach(copy_list, function (cp) {
+            $modal.open({
+                templateUrl: './cat/share/t_replace_barcode',
+                animation: true,
+                controller:
+                           ['$scope','$modalInstance',
+                    function($scope , $modalInstance) {
+                        $scope.isModal = true;
+                        $scope.focusBarcode = false;
+                        $scope.focusBarcode2 = true;
+                        $scope.barcode1 = cp.barcode();
+
+                        $scope.updateBarcode = function() {
+                            $scope.copyNotFound = false;
+                            $scope.updateOK = false;
+                
+                            egCore.pcrud.search('acp',
+                                {deleted : 'f', barcode : $scope.barcode1})
+                            .then(function(copy) {
+                
+                                if (!copy) {
+                                    $scope.focusBarcode = true;
+                                    $scope.copyNotFound = true;
+                                    return;
+                                }
+                
+                                $scope.copyId = copy.id();
+                                copy.barcode($scope.barcode2);
+                
+                                egCore.pcrud.update(copy).then(function(stat) {
+                                    $scope.updateOK = stat;
+                                    $scope.focusBarcode = true;
+                                    holdingsSvc.fetchAgain().then(function (){
+                                        holdingsGridDataProviderRef.refresh();
+                                    });
+                                });
+
+                            });
+                            $modalInstance.close();
+                        }
+
+                        $scope.cancel = function($event) {
+                            $modalInstance.dismiss();
+                            $event.preventDefault();
+                        }
+                    }
+                ]
+            });
+        });
+    }
+
     // refresh the list of holdings when the record_id is changed.
     $scope.holdings_record_id_changed = function(id) {
         if ($scope.record_id != id) $scope.record_id = id;
@@ -806,13 +863,7 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
                 }
             ).then(function(success) {
                 if (success) {
-                    holdingsSvc.fetch({
-                        rid : $scope.record_id,
-                        org : $scope.holdings_ou,
-                        copy: $scope.holdings_show_copies,
-                        vol : $scope.holdings_show_vols,
-                        empty: $scope.holdings_show_empty
-                    }).then(function() {
+                    holdingsSvc.fetchAgain().then(function() {
                         $scope.holdingsGridDataProvider.refresh();
                     });
                 } else {
@@ -836,13 +887,7 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
                 copy_list
             ).then(function(success) {
                 if (success) {
-                    holdingsSvc.fetch({
-                        rid : $scope.record_id,
-                        org : $scope.holdings_ou,
-                        copy: $scope.holdings_show_copies,
-                        vol : $scope.holdings_show_vols,
-                        empty: $scope.holdings_show_empty
-                    }).then(function() {
+                    holdingsSvc.fetchAgain().then(function() {
                         $scope.holdingsGridDataProvider.refresh();
                     });
                 } else {
@@ -877,13 +922,7 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
 
     $scope.selectedHoldingsDamaged = function () {
         egCirc.mark_damaged(gatherSelectedHoldingsIds()).then(function() {
-            holdingsSvc.fetch({
-                rid : $scope.record_id,
-                org : $scope.holdings_ou,
-                copy: $scope.holdings_show_copies,
-                vol : $scope.holdings_show_vols,
-                empty: $scope.holdings_show_empty
-            }).then(function() {
+            holdingsSvc.fetchAgain().then(function() {
                 $scope.holdingsGridDataProvider.refresh();
             });
         });
@@ -891,13 +930,7 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
 
     $scope.selectedHoldingsMissing = function () {
         egCirc.mark_missing(gatherSelectedHoldingsIds()).then(function() {
-            holdingsSvc.fetch({
-                rid : $scope.record_id,
-                org : $scope.holdings_ou,
-                copy: $scope.holdings_show_copies,
-                vol : $scope.holdings_show_vols,
-                empty: $scope.holdings_show_empty
-            }).then(function() {
+            holdingsSvc.fetchAgain().then(function() {
                 $scope.holdingsGridDataProvider.refresh();
             });
         });
@@ -1183,6 +1216,16 @@ function(egCore , $q) {
         }
     }
 
+    service.fetchAgain = function() {
+        return service.fetch({
+            rid: service.rid,
+            org: service.org,
+            copy: service.copy,
+            vol: service.vol,
+            empty: service.empty
+        })
+    }
+
     // resolved with the last received copy
     service.fetch = function(opts) {
         if (service.ongoing) {
@@ -1203,6 +1246,10 @@ function(egCore , $q) {
 
         service.rid = rid;
         service.org = org;
+        service.copy = opts.copy;
+        service.vol = opts.vol;
+        service.empty = opts.empty;
+
         service.copies = [];
         service.index = 0;
 

commit ba4b2921846e66a4fc22ca2ccd7a21a818299ef2
Author: Mike Rylander <mrylander at gmail.com>
Date:   Wed Sep 9 10:17:05 2015 -0400

    webstaff: Conjoined items management
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/catalog/index.tt2 b/Open-ILS/src/templates/staff/cat/catalog/index.tt2
index 9635c5d..f5db2fe 100644
--- a/Open-ILS/src/templates/staff/cat/catalog/index.tt2
+++ b/Open-ILS/src/templates/staff/cat/catalog/index.tt2
@@ -22,6 +22,10 @@
       "[% l('Permanently delete selected copies and/or volumes from catalog?') %]";
     s.CONFIRM_DELETE_COPIES_VOLUMES_MESSAGE =
       "[% l('Will delete {{copies}} copies and {{volumes}} volumes') %]";
+    s.CONFIRM_DELETE_PEERS =
+      "[% l('Unlink selected conjoined copies?') %]";
+    s.CONFIRM_DELETE_PEERS_MESSAGE =
+      "[% l('Will unlink {{peers}} copies') %]";
   }])
 </script>
 
diff --git a/Open-ILS/src/templates/staff/cat/catalog/t_catalog.tt2 b/Open-ILS/src/templates/staff/cat/catalog/t_catalog.tt2
index 9b5eaba..62db3ca 100644
--- a/Open-ILS/src/templates/staff/cat/catalog/t_catalog.tt2
+++ b/Open-ILS/src/templates/staff/cat/catalog/t_catalog.tt2
@@ -59,6 +59,11 @@
         [% l('Holdings View') %]
     </a>
   </li>
+  <li ng-class="{disabled : !record_id, active : record_tab == 'conjoined'}">
+    <a ng-click="set_record_tab('conjoined')" >
+        [% l('Conjoined Items') %]
+    </a>
+  </li>
 </ul>
 
 <div class="tab-content">
@@ -86,5 +91,8 @@
     <div ng-if="record_tab == 'holdings'">
       [% INCLUDE 'staff/cat/catalog/t_holdings.tt2' %]
     </div>
+    <div ng-if="record_tab == 'conjoined'">
+      [% INCLUDE 'staff/cat/catalog/t_conjoined_items.tt2' %]
+    </div>
   </div>
 </div>
diff --git a/Open-ILS/src/templates/staff/cat/catalog/t_conjoined_items.tt2 b/Open-ILS/src/templates/staff/cat/catalog/t_conjoined_items.tt2
new file mode 100644
index 0000000..a4ad8c9
--- /dev/null
+++ b/Open-ILS/src/templates/staff/cat/catalog/t_conjoined_items.tt2
@@ -0,0 +1,35 @@
+
+<div>
+  <eg-grid
+    id-field="id"
+    idl-class="bpbcm"
+    features="-display,-sort,-multisort,-pagination,-picker,-actions"
+    items-provider="conjoinedGridDataProvider"
+    grid-controls="conjoinedGridControls"
+    persist-key="cat.peer_bibs">
+
+     <eg-grid-menu-item handler="refreshConjoined"
+      label="[% l('Refresh') %]"/>
+ 
+     <eg-grid-menu-item handler="deleteSelectedConjoined"
+      label="[% l('Unlink') %]"/>
+ 
+    <eg-grid-menu-item handler="changeConjoinedType"
+      label="[% l('Change Type') %]"/>
+
+    <eg-grid-field label="[% l('Copy') %]"  path="target_copy.barcode" visible>
+      <a target="_self" href="[% ctx.base_path %]/staff/cat/item/{{item.target_copy().id()}}">
+        {{item.target_copy().barcode()}}
+      </a>
+    </eg-grid-field>
+    <eg-grid-field label="[% l('Title') %]"  path="target_copy.call_number.record.simple_record.title" visible>
+      <a target="_self" href="[% ctx.base_path %]/staff/cat/catalog/record/{{item.target_copy().call_number().record().id()}}">
+        {{item.target_copy().call_number().record().simple_record().title()}}
+      </a>
+    </eg-grid-field>
+    <eg-grid-field label="[% l('Type') %]"  path="peer_type.name" visible></eg-grid-field>
+    <eg-grid-field label="[% l('ID') %]"  path="peer_type.id" ></eg-grid-field>
+  
+  </eg-grid>
+</div>
+  
diff --git a/Open-ILS/src/templates/staff/cat/catalog/t_conjoined_selector.tt2 b/Open-ILS/src/templates/staff/cat/catalog/t_conjoined_selector.tt2
index 377c426..aac05ae 100644
--- a/Open-ILS/src/templates/staff/cat/catalog/t_conjoined_selector.tt2
+++ b/Open-ILS/src/templates/staff/cat/catalog/t_conjoined_selector.tt2
@@ -2,7 +2,8 @@
     <div class="modal-header">
       <button type="button" class="close" ng-click="cancel()" 
         aria-hidden="true">×</button>
-      <h4 class="modal-title">[% l('Attach conjoined items') %]</h4>
+      <h4 ng-if="update" class="modal-title">[% l('Update conjoined items') %]</h4>
+      <h4 ng-if="!update" class="modal-title">[% l('Attach conjoined items') %]</h4>
     </div>
     <div class="modal-body">
       <div class="row">
diff --git a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
index a5331d7..20fe682 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
@@ -227,9 +227,9 @@ function($scope , $routeParams , $location , $window , $q , egCore) {
 
 .controller('CatalogCtrl',
        ['$scope','$routeParams','$location','$window','$q','egCore','egHolds','egCirc','egConfirmDialog',
-        'egGridDataProvider','egHoldGridActions','$timeout','$modal','holdingsSvc','egUser',
+        'egGridDataProvider','egHoldGridActions','$timeout','$modal','holdingsSvc','egUser','conjoinedSvc',
 function($scope , $routeParams , $location , $window , $q , egCore , egHolds , egCirc,  egConfirmDialog,
-         egGridDataProvider , egHoldGridActions , $timeout , $modal , holdingsSvc , egUser) {
+         egGridDataProvider , egHoldGridActions , $timeout , $modal , holdingsSvc , egUser , conjoinedSvc) {
 
     // set record ID on page load if available...
     $scope.record_id = $routeParams.record_id;
@@ -338,6 +338,9 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
             $scope.record_id = match[1];
             egCore.hatch.setLocalItem("eg.cat.last_record_retrieved", $scope.record_id);
             $scope.holdings_record_id_changed($scope.record_id);
+            conjoinedSvc.fetch($scope.record_id).then(function(){
+                $scope.conjoinedGridDataProvider.refresh();
+            });
             init_parts_url();
         } else {
             delete $scope.record_id;
@@ -364,6 +367,92 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
     $scope.handlers = { }
 
     // ------------------------------------------------------------------
+    // Conjoined items
+
+    $scope.conjoinedGridControls = {};
+    $scope.conjoinedGridDataProvider = egGridDataProvider.instance({
+        get : function(offset, count) {
+            return this.arrayNotifier(conjoinedSvc.items, offset, count);
+        }
+    });
+
+    $scope.changeConjoinedType = function () {
+        var peers = egCore.idl.Clone($scope.conjoinedGridControls.selectedItems());
+        angular.forEach(peers, function (p) {
+            p.target_copy(p.target_copy().id());
+            p.peer_type(p.peer_type().id());
+        });
+
+        var conjoinedGridDataProviderRef = $scope.conjoinedGridDataProvider;
+
+        return $modal.open({
+            templateUrl: './cat/catalog/t_conjoined_selector',
+            animation: true,
+            controller:
+                   ['$scope','$modalInstance',
+            function($scope , $modalInstance) {
+                $scope.update = true;
+
+                $scope.peer_type = null;
+                $scope.peer_type_list = [];
+                conjoinedSvc.get_peer_types().then(function(list){
+                    $scope.peer_type_list = list;
+                });
+    
+                $scope.ok = function(type) {
+                    var promises = [];
+    
+                    angular.forEach(peers, function (p) {
+                        p.ischanged(1);
+                        p.peer_type(type);
+                        promises.push(egCore.pcrud.update(p));
+                    });
+    
+                    return $q.all(promises)
+                        .then(function(){$modalInstance.close()})
+                        .then(function(){return conjoinedSvc.fetch()})
+                        .then(function(){conjoinedGridDataProviderRef.refresh()});
+                }
+    
+                $scope.cancel = function($event) {
+                    $modalInstance.dismiss();
+                    $event.preventDefault();
+                }
+            }]
+        });
+        
+    }
+
+    $scope.refreshConjoined = function () {
+        conjoinedSvc.fetch($scope.record_id)
+        .then(function(){$scope.conjoinedGridDataProvider.refresh();});
+    }
+
+    $scope.deleteSelectedConjoined = function () {
+        var peers = $scope.conjoinedGridControls.selectedItems();
+
+        if (peers.length > 0) {
+            egConfirmDialog.open(
+                egCore.strings.CONFIRM_DELETE_PEERS,
+                egCore.strings.CONFIRM_DELETE_PEERS_MESSAGE,
+                {peers : peers.length}
+            ).result.then(function() {
+                angular.forEach(peers, function (p) {
+                    p.isdeleted(1);
+                });
+
+                egCore.pcrud.remove(peers).then(function() {
+                    return conjoinedSvc.fetch();
+                }).then(function() {
+                    $scope.conjoinedGridDataProvider.refresh();
+                });
+            });
+        }
+    }
+    if ($scope.record_id)
+        conjoinedSvc.fetch($scope.record_id);
+
+    // ------------------------------------------------------------------
     // Holdings
 
     $scope.holdingsGridControls = {};
@@ -827,9 +916,11 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
                 controller:
                        ['$scope','$modalInstance',
                 function($scope , $modalInstance) {
+                    $scope.update = false;
+
                     $scope.peer_type = null;
                     $scope.peer_type_list = [];
-                    holdingsSvc.get_peer_types().then(function(list){
+                    conjoinedSvc.get_peer_types().then(function(list){
                         $scope.peer_type_list = list;
                     });
     
@@ -1328,6 +1419,53 @@ function(egCore , $q) {
         );
     }
 
+    return service;
+}])
+
+.factory('conjoinedSvc', 
+       ['egCore','$q',
+function(egCore , $q) {
+
+    var service = {
+        items : [], // record search results
+        index : 0, // search grid index
+        rid : null
+    };
+
+    service.flesh = {   
+        flesh : 4, 
+        flesh_fields : {
+            bpbcm : ['target_copy','peer_type'],
+            acp : ['call_number'],
+            acn : ['record'],
+            bre : ['simple_record']
+        },
+        // avoid fetching the MARC blob by specifying which
+        // fields on the bre to select.  More may be needed.
+        // note that fleshed fields are explicitly selected.
+        select : { bre : ['id'] },
+        order_by : { bpbcm : ['id'] },
+    }
+
+    // resolved with the last received copy
+    service.fetch = function(rid) {
+        if (!rid && !service.rid) return $q.when();
+
+        if (rid) service.rid = rid;
+        service.items = [];
+        service.index = 0;
+
+        return egCore.pcrud.search(
+            'bpbcm',
+            {peer_record : service.rid},
+            service.flesh,
+            {atomic : true}
+        ).then( function(list) { // finished
+            service.items = list;
+            return service.items;
+        });
+    }
+
     // returns a promise resolved with the list of peer bib types
     service.get_peer_types = function() {
         if (egCore.env.bpt)

commit 1c95ebadc6c60b5a977bcc7cca11eb1e65b31217
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Tue Sep 8 21:47:49 2015 +0000

    webstaff: ensure that side-by-side MARC record views don't overlap text
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/css/cat.css.tt2 b/Open-ILS/src/templates/staff/css/cat.css.tt2
index f3f27d2..ef3cd9c 100644
--- a/Open-ILS/src/templates/staff/css/cat.css.tt2
+++ b/Open-ILS/src/templates/staff/css/cat.css.tt2
@@ -150,3 +150,12 @@ grid[name="-none-"] * label { color: black; }
 #z3950-search-form-row {
     margin-bottom: 20px;
 }
+
+/*
+ *  MARC view styles
+ */
+
+/* ensure that side-by-side MARC record views don't overlap text */
+.marc_tag_row {
+    word-break: break-word;
+}

commit 17607305ba338bdfca3200038cfe1add87fd384e
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Tue Sep 8 21:42:49 2015 +0000

    webstaff: Z39.50: unbreak MARC editor embedded in overlay
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/z3950/t_edit_overlay_record.tt2 b/Open-ILS/src/templates/staff/cat/z3950/t_edit_overlay_record.tt2
index 3372bcf..e99fb42 100644
--- a/Open-ILS/src/templates/staff/cat/z3950/t_edit_overlay_record.tt2
+++ b/Open-ILS/src/templates/staff/cat/z3950/t_edit_overlay_record.tt2
@@ -6,7 +6,7 @@
   </div>
   <div class="modal-body">
     <eg-marc-edit-record dirty-flag="dirty_flag" record-id="record_id" marc-xml="args.marc_xml"
-                         in-place-mode="true" />
+                         in-place-mode="true" record-type="bre" />
   </div>
   <div class="modal-footer">
     <input type="submit" ng-click="ok(args)"

commit 00cfa3bb3aed1c7676bb1a62b210e57a09236ff1
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Tue Sep 8 20:44:00 2015 +0000

    webstaff: flat MARC editor now updates model on blur, not change
    
    This fixes a problem where attempting to type in the flat
    MARC editor would result in a fromBreaker()/toBreaker() cycle
    being run with each keystroke, which (among other things) made
    the user have to fight for control of the insertion cursor.
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/share/t_marcedit.tt2 b/Open-ILS/src/templates/staff/cat/share/t_marcedit.tt2
index dd6932c..59596a5 100644
--- a/Open-ILS/src/templates/staff/cat/share/t_marcedit.tt2
+++ b/Open-ILS/src/templates/staff/cat/share/t_marcedit.tt2
@@ -58,7 +58,7 @@
   </div>
 
   <div ng-show="flatEditor">
-    <textarea cols="120" rows="40" ng-model="flat_text_marc" ng-change="saveFlatTextMARC()"></textarea>
+    <textarea cols="120" rows="40" ng-model="flat_text_marc" ng-blur="saveFlatTextMARC()"></textarea>
   </div>
   <div ng-show="!flatEditor">
     <div class="row pad-vert">

commit 2ed294bd548d5031ac1b6099077a581754f12daf
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Tue Sep 8 19:33:00 2015 +0000

    webstaff: fix a thinko when saving default Z39.50 targets
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/cat/services/z3950.js b/Open-ILS/web/js/ui/default/staff/cat/services/z3950.js
index 11b5d94..918de4b 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/services/z3950.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/services/z3950.js
@@ -131,10 +131,10 @@ function($q,   egCore,   egAuth) {
         angular.forEach(service.targets, function(target, idx) {
             if (target.selected) {
                 saved_targets[target.code] = {};
-            }
-            if (target.settings.auth == 't') {
-                saved_targets[target.code]['username'] = target.username;
-                saved_targets[target.code]['password'] = target.password;
+                if (target.settings.auth == 't') {
+                    saved_targets[target.code]['username'] = target.username;
+                    saved_targets[target.code]['password'] = target.password;
+                }
             }
         }); 
         egCore.hatch.setLocalItem('eg.cat.z3950.default_targets', saved_targets);

commit bac309deb5de1013ddbeb7e5e516fde9f99cc461
Author: Jason Etheridge <jason at esilibrary.com>
Date:   Tue Sep 8 14:10:33 2015 -0400

    webstaff Z39.50: don't risk letting default_targets be undefined
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/cat/services/z3950.js b/Open-ILS/web/js/ui/default/staff/cat/services/z3950.js
index 9e4b8ff..11b5d94 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/services/z3950.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/services/z3950.js
@@ -10,7 +10,7 @@ function($q,   egCore,   egAuth) {
     };
     
     service.loadTargets = function() {
-        var default_targets = egCore.hatch.getLocalItem('eg.cat.z3950.default_targets');
+        var default_targets = egCore.hatch.getLocalItem('eg.cat.z3950.default_targets') || [];
         egCore.net.request(
             'open-ils.search',
             'open-ils.search.z3950.retrieve_services',

commit 5a91eb9905ae934ede77bbdcd3f919f8349d264b
Author: Mike Rylander <mrylander at gmail.com>
Date:   Tue Sep 8 15:27:22 2015 -0400

    webstaff: Conjoined item attach-inator
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/catalog/t_conjoined_selector.tt2 b/Open-ILS/src/templates/staff/cat/catalog/t_conjoined_selector.tt2
new file mode 100644
index 0000000..377c426
--- /dev/null
+++ b/Open-ILS/src/templates/staff/cat/catalog/t_conjoined_selector.tt2
@@ -0,0 +1,25 @@
+<form ng-submit="ok(type)" role="form">
+    <div class="modal-header">
+      <button type="button" class="close" ng-click="cancel()" 
+        aria-hidden="true">×</button>
+      <h4 class="modal-title">[% l('Attach conjoined items') %]</h4>
+    </div>
+    <div class="modal-body">
+      <div class="row">
+        <div class="col-md-6">
+            <b>[% l('Peer Type:') %]</b>
+        </div>
+        <div class="col-md-6">
+          <select class="form-control" ng-options="t.id() as t.name() for t in peer_type_list" ng-model="type"></select>
+        </div>
+      </div>
+    </div>
+    <div class="modal-footer">
+      <div class="row">
+        <div class="col-md-12 pull-right">
+          <input type="submit" class="btn btn-primary" value="[% l('OK') %]"/>
+          <button class="btn btn-warning" ng-click="cancel($event)">[% l('Cancel') %]</button>
+        </div>
+      </div>
+    </div>
+</form>
diff --git a/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2 b/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2
index c5ab768..c7a1142 100644
--- a/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2
+++ b/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2
@@ -35,6 +35,8 @@
 
     <eg-grid-action handler="requestItems"
       label="[% l('Request Items') %]"></eg-grid-action>
+    <eg-grid-action handler="attach_to_peer_bib"
+      label="[% l('Link as Conjoined to Previously Marked Bib Record') %]"></eg-grid-action>
 
     <eg-grid-action handler="selectedHoldingsItemStatus" group="[% l('Show') %]"
       label="[% l('Item Status (list)') %]"></eg-grid-action>
diff --git a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
index 5c8f917..a5331d7 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
@@ -814,6 +814,49 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
         });
     }
 
+    $scope.attach_to_peer_bib = function() {
+        var copy_list = gatherSelectedHoldingsIds();
+        if (copy_list.length == 0) return;
+
+        egCore.hatch.getItem('eg.cat.marked_conjoined_record').then(function(target_record) {
+            if (!target_record) return;
+
+            return $modal.open({
+                templateUrl: './cat/catalog/t_conjoined_selector',
+                animation: true,
+                controller:
+                       ['$scope','$modalInstance',
+                function($scope , $modalInstance) {
+                    $scope.peer_type = null;
+                    $scope.peer_type_list = [];
+                    holdingsSvc.get_peer_types().then(function(list){
+                        $scope.peer_type_list = list;
+                    });
+    
+                    $scope.ok = function(type) {
+                        var promises = [];
+    
+                        angular.forEach(copy_list, function (cp) {
+                            var n = new egCore.idl.bpbcm();
+                            n.isnew(true);
+                            n.peer_record(target_record);
+                            n.target_copy(cp);
+                            n.peer_type(type);
+                            promises.push(egCore.pcrud.create(n));
+                        });
+    
+                        return $q.all(promises).then(function(){$modalInstance.close()});
+                    }
+    
+                    $scope.cancel = function($event) {
+                        $modalInstance.dismiss();
+                        $event.preventDefault();
+                    }
+                }]
+            });
+        });
+    }
+
 
     // ------------------------------------------------------------------
     // Holds 
@@ -1285,6 +1328,18 @@ function(egCore , $q) {
         );
     }
 
+    // returns a promise resolved with the list of peer bib types
+    service.get_peer_types = function() {
+        if (egCore.env.bpt)
+            return $q.when(egCore.env.bpt.list);
+
+        return egCore.pcrud.retrieveAll('bpt', null, {atomic : true})
+        .then(function(list) {
+            egCore.env.absorbList(list, 'bpt');
+            return list;
+        });
+    };
+
     return service;
 }])
 

commit 58eae9ebb53d65da27cbf9aac9e497069dd69833
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Tue Sep 8 19:16:28 2015 +0000

    webstaff: implement add to record bucket from catalog view
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/catalog/t_add_to_bucket.tt2 b/Open-ILS/src/templates/staff/cat/catalog/t_add_to_bucket.tt2
new file mode 100644
index 0000000..eb53d04
--- /dev/null
+++ b/Open-ILS/src/templates/staff/cat/catalog/t_add_to_bucket.tt2
@@ -0,0 +1,34 @@
+<div class="modal-header">
+  <button type="button" class="close" ng-click="cancel()"
+    aria-hidden="true">×</button>
+  <h4 class="modal-title">[% l('Add To Bucket') %]</h4>
+</div>
+<div class="modal-body">
+  <div class="row">
+    <div class="form-inline">
+      <div class="form-gruop">
+        <label for="select-bucket">[% l('Copy this record into which bucket?') %]</label>
+        <select id="select-bucket" class="form-control" ng-model="bucket_id"
+                ng-options="bucket.name() for bucket in allBuckets track by bucket.id()">
+        </select>
+        <button class="btn btn-primary" ng-class="{disabled : !bucket_id}" 
+                ng-click="add_to_bucket()">[% l('Add To Selected Bucket') %]</button>
+      </div>
+    </div>
+  </div>
+  <div class="row">
+    <div class="form-inline">
+      <div class="form-gruop">
+        <label for="new-bucket-name">[% l('Name For New Bucket') %]</label>
+        <input type="text" class="form-control" id="new-bucket-name" ng-model="newBucketName" />
+        <button class="btn btn-primary" ng-class="{disabled : !newBucketName}"
+                ng-click="add_to_new_bucket()">[% l('Add To New Bucket') %]</button>
+      </div>
+  </div>
+  </div>
+</div>
+<div class="modal-footer">
+  <div class="col-md-6 pull-right">
+    <button class="btn btn-warning" ng-click="cancel($event)">[% l('Cancel') %]</button>
+  </div>
+</div>
diff --git a/Open-ILS/src/templates/staff/cat/catalog/t_catalog.tt2 b/Open-ILS/src/templates/staff/cat/catalog/t_catalog.tt2
index f7477b1..9b5eaba 100644
--- a/Open-ILS/src/templates/staff/cat/catalog/t_catalog.tt2
+++ b/Open-ILS/src/templates/staff/cat/catalog/t_catalog.tt2
@@ -21,6 +21,11 @@
        [% l('Back To Results') %]
     </button>
   </div>
+  <div class="col-md-3">
+    <button type="button" class="btn btn-default" ng-click="add_to_record_bucket()">
+        [% l('Add To Bucket') %]
+    </button>
+  </div>
 </div>
 
 <ul class="nav nav-tabs">
diff --git a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
index 4177943..5c8f917 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
@@ -250,6 +250,63 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
         }
     }
 
+    $scope.add_to_record_bucket = function() {
+        var recId = $scope.record_id;
+        return $modal.open({
+            templateUrl: './cat/catalog/t_add_to_bucket',
+            animation: true,
+            size: 'md',
+            controller:
+                   ['$scope','$modalInstance',
+            function($scope , $modalInstance) {
+
+                $scope.bucket_id = 0;
+                $scope.newBucketName = '';
+                $scope.allBuckets = [];
+                egCore.net.request(
+                    'open-ils.actor',
+                    'open-ils.actor.container.retrieve_by_class.authoritative',
+                    egCore.auth.token(), egCore.auth.user().id(),
+                    'biblio', 'staff_client'
+                ).then(function(buckets) { $scope.allBuckets = buckets; });
+
+                $scope.add_to_bucket = function() {
+                    var item = new egCore.idl.cbrebi();
+                    item.bucket($scope.bucket_id);
+                    item.target_biblio_record_entry(recId);
+                    egCore.net.request(
+                        'open-ils.actor',
+                        'open-ils.actor.container.item.create',
+                        egCore.auth.token(), 'biblio', item
+                    ).then(function(resp) {
+                        $modalInstance.close();
+                    });
+                }
+
+                $scope.add_to_new_bucket = function() {
+                    var bucket = new egCore.idl.cbreb();
+                    bucket.owner(egCore.auth.user().id());
+                    bucket.name($scope.newBucketName);
+                    bucket.description('');
+                    bucket.btype('staff_client');
+
+                    egCore.net.request(
+                        'open-ils.actor',
+                        'open-ils.actor.container.create',
+                        egCore.auth.token(), 'biblio', bucket
+                    ).then(function(bucket) {
+                        $scope.bucket_id = bucket;
+                        $scope.add_to_bucket();
+                    });
+                }
+
+                $scope.cancel = function() {
+                    $modalInstance.dismiss();
+                }
+            }]
+        });
+    }
+
     $scope.stop_unload = false;
     $scope.$watch('stop_unload',
         function(newVal, oldVal) {

commit 00815ad5e90a6919c7dae24b8dfdf46a5b592685
Author: Mike Rylander <mrylander at gmail.com>
Date:   Tue Sep 8 14:42:36 2015 -0400

    webstaff: More default values for required fields
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
index 27a2d36..fde2e06 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
@@ -424,7 +424,10 @@ function(egCore , $q) {
                 '<div class="col-xs-1"><eg-org-selector selected="owning_lib" disableTest="cant_have_vols"></eg-org-selector></div>'+
                 '<div class="col-xs-1"><input class="form-control" type="number" min="{{orig_cn_count}}" ng-model="cn_count" ng-change="changeCNCount()"/></div>'+
                 '<div class="col-xs-10">'+
-                    '<eg-vol-row ng-repeat="(cn,copies) in struct | orderBy:cn track by cn" focus-next="focusNextFirst" copies="copies" allcopies="allcopies"></eg-vol-row>'+
+                    '<eg-vol-row '+
+                        'ng-repeat="(cn,copies) in struct | orderBy:cn track by cn" '+
+                        'focus-next="focusNextFirst" copies="copies" allcopies="allcopies">'+
+                    '</eg-vol-row>'+
                 '</div>'+
             '</div>',
 
@@ -494,8 +497,22 @@ function(egCore , $q) {
                             cn.record( $scope.full_cn.record() );
 
                             var cp = new egCore.idl.acp();
+                            cp.call_number( cn );
                             cp.id( --itemSvc.new_cp_id );
                             cp.isnew( true );
+
+                            cp.deposit(0);
+                            cp.price(0);
+                            cp.deposit_amount(0);
+                            cp.fine_level(2); // Normal
+                            cp.loan_duration(2); // Normal
+                            cp.location(1); // Stacks
+                            cp.circulate('t');
+                            cp.holdable('t');
+                            cp.opac_visible('t');
+                            cp.ref('f');
+                            cp.mint_condition('t');
+
                             cp.circ_lib( $scope.owning_lib.id() );
                             cp.call_number( cn );
 
@@ -865,7 +882,20 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
                                     cp.call_number( cn );
                                     cp.id( --itemSvc.new_cp_id );
                                     cp.isnew( true );
-                                    cp.circ_lib( $scope.record_id );
+                                    cp.circ_lib( proto.owner || egCore.auth.user().ws_ou() );
+
+                                    cp.deposit(0);
+                                    cp.price(0);
+                                    cp.deposit_amount(0);
+                                    cp.fine_level(2); // Normal
+                                    cp.loan_duration(2); // Normal
+                                    cp.location(1); // Stacks
+                                    cp.circulate('t');
+                                    cp.holdable('t');
+                                    cp.opac_visible('t');
+                                    cp.ref('f');
+                                    cp.mint_condition('t');
+
                                     if (proto.barcode) cp.barcode( proto.barcode );
 
                                     itemSvc.addCopy(cp)
@@ -885,7 +915,20 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
                                 cp.call_number( cn );
                                 cp.id( --itemSvc.new_cp_id );
                                 cp.isnew( true );
-                                cp.circ_lib( $scope.record_id );
+
+                                cp.deposit(0);
+                                cp.price(0);
+                                cp.deposit_amount(0);
+                                cp.fine_level(2); // Normal
+                                cp.loan_duration(2); // Normal
+                                cp.location(1); // Stacks
+                                cp.circulate('t');
+                                cp.holdable('t');
+                                cp.opac_visible('t');
+                                cp.ref('f');
+                                cp.mint_condition('t');
+
+                                cp.circ_lib( proto.owner || egCore.auth.user().ws_ou() );
                                 if (proto.barcode) cp.barcode( proto.barcode );
 
                                 itemSvc.addCopy(cp)
@@ -899,12 +942,12 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
             }
 
         }).then( function() {
+            $scope.data = itemSvc;
             if ($scope.add_vols_copies) {
                 egCore.org.settings([
                     'cat.default_copy_status_fast'
                 ]).then(function(set) {
                     $scope.fast_ccs = set['cat.default_copy_status_fast'] || 0;
-                    $scope.data = itemSvc;
                     angular.forEach($scope.data.copies, function (cp) {
                         cp.status($scope.fast_ccs);
                     });

commit 863e9ea11b3f1528fbc7ccbab24716aadb7a351f
Author: Mike Rylander <mrylander at gmail.com>
Date:   Tue Sep 8 12:38:24 2015 -0400

    webstaff: Add support for the fast-add copy status org setting
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
index 235a37a..27a2d36 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
@@ -823,6 +823,7 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
         });
 
         $scope.workingGridControls = {};
+        $scope.add_vols_copies = false;
 
         egNet.request(
             'open-ils.actor',
@@ -841,6 +842,7 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
 
                 if (data.raw && data.raw.length) {
                     $scope.dirty = true;
+                    $scope.add_vols_copies = true;
 
                     /* data.raw data structure looks like this:
                      * [{
@@ -897,8 +899,18 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
             }
 
         }).then( function() {
-            $scope.data = itemSvc;
-            $scope.workingGridDataProvider.refresh();
+            if ($scope.add_vols_copies) {
+                egCore.org.settings([
+                    'cat.default_copy_status_fast'
+                ]).then(function(set) {
+                    $scope.fast_ccs = set['cat.default_copy_status_fast'] || 0;
+                    $scope.data = itemSvc;
+                    angular.forEach($scope.data.copies, function (cp) {
+                        cp.status($scope.fast_ccs);
+                    });
+                    $scope.workingGridDataProvider.refresh();
+                });
+            }
         });
 
         $scope.focusNextFirst = function(prev_lib) {

commit f6eb26fa807c3ad5f0c01f1271313daca98efc0f
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Tue Sep 8 16:07:36 2015 +0000

    webstaff: implement saving default Z39.50 targets
    
    Also make the target password input field's type be
    "password".
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/z3950/t_list.tt2 b/Open-ILS/src/templates/staff/cat/z3950/t_list.tt2
index 86292c9..5e2ab8b 100644
--- a/Open-ILS/src/templates/staff/cat/z3950/t_list.tt2
+++ b/Open-ILS/src/templates/staff/cat/z3950/t_list.tt2
@@ -20,7 +20,12 @@
     </div>
     <div class="col-xs-6">
         <strong>[% l('Service and Credentials') %]</strong>
-        <eg-z3950-target-list ng-show="show_search_form()">
+        <eg-z3950-target-list ng-show="show_search_form()"></eg-z3950-target-list>
+        <div class="button-group">
+            <button class="btn btn-default" ng-click="saveDefaultZ3950Targets()">
+                [% l('Save as Default') %]
+            </button>
+        </div>
     </div>
 </div>
 
diff --git a/Open-ILS/src/templates/staff/cat/z3950/t_target.tt2 b/Open-ILS/src/templates/staff/cat/z3950/t_target.tt2
index 9365ec2..17ca475 100644
--- a/Open-ILS/src/templates/staff/cat/z3950/t_target.tt2
+++ b/Open-ILS/src/templates/staff/cat/z3950/t_target.tt2
@@ -11,7 +11,7 @@
         </div>
         <div class="form-group col-xs-6">
             <label for="password-for-z3950-{{target.code}}">[% l('Password') %]</label>
-            <input type="text" class="form-control" id="password-for-z3950-{{target.code}}" ng-model="target.password">
+            <input type="password" class="form-control" id="password-for-z3950-{{target.code}}" ng-model="target.password">
         </div>
     </div>
 </div>
diff --git a/Open-ILS/web/js/ui/default/staff/cat/services/z3950.js b/Open-ILS/web/js/ui/default/staff/cat/services/z3950.js
index 45969be..9e4b8ff 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/services/z3950.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/services/z3950.js
@@ -10,6 +10,7 @@ function($q,   egCore,   egAuth) {
     };
     
     service.loadTargets = function() {
+        var default_targets = egCore.hatch.getLocalItem('eg.cat.z3950.default_targets');
         egCore.net.request(
             'open-ils.search',
             'open-ils.search.z3950.retrieve_services',
@@ -21,13 +22,18 @@ function($q,   egCore,   egAuth) {
             var localTarget = res['native-evergreen-catalog'];
             delete res['native-evergreen-catalog'];
             angular.forEach(res, function(value, key) {
-                this.push({
+                var tgt = {
                     code:       key,
                     settings:   value,
-                    selected:   false,
+                    selected:   (key in default_targets),
                     username:   '',
                     password:   ''
-                });
+                };
+                if (tgt.code in default_targets && tgt.settings.auth == 't') {
+                    tgt['username'] = default_targets[tgt.code]['username'] || '';
+                    tgt['password'] = default_targets[tgt.code]['password'] || '';
+                }
+                this.push(tgt);
             }, service.targets);
             service.targets.sort(function (a, b) {
                 a = a.settings.label;
@@ -37,7 +43,7 @@ function($q,   egCore,   egAuth) {
             service.targets.unshift({
                 code:       'native-evergreen-catalog',
                 settings:   localTarget,
-                selected:   false,
+                selected:   ('native-evergreen-catalog' in default_targets),
                 username:   '',
                 password:   ''
             });
@@ -119,6 +125,21 @@ function($q,   egCore,   egAuth) {
         service.raw_search = raw_search;
     }
 
+    // store selected targets
+    service.saveDefaultZ3950Targets = function() {
+        var saved_targets = {};
+        angular.forEach(service.targets, function(target, idx) {
+            if (target.selected) {
+                saved_targets[target.code] = {};
+            }
+            if (target.settings.auth == 't') {
+                saved_targets[target.code]['username'] = target.username;
+                saved_targets[target.code]['password'] = target.password;
+            }
+        }); 
+        egCore.hatch.setLocalItem('eg.cat.z3950.default_targets', saved_targets);
+    }
+
     return service;
 }])
 .directive("egZ3950TargetList", function () {
diff --git a/Open-ILS/web/js/ui/default/staff/cat/z3950/app.js b/Open-ILS/web/js/ui/default/staff/cat/z3950/app.js
index 5476c71..3d8d9d1 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/z3950/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/z3950/app.js
@@ -131,6 +131,10 @@ function($scope , $q , $location , $timeout , $window,  egCore , egGridDataProvi
         egZ3950TargetSvc.clearSearchFields();
     };
 
+    $scope.saveDefaultZ3950Targets = function() {
+        egZ3950TargetSvc.saveDefaultZ3950Targets();
+    }
+
     var display_form = true;
     $scope.show_search_form = function() {
         return display_form;

commit ebbf072d1e69cf0a255ad60bfc2fdecf979a41ab
Author: Mike Rylander <mrylander at gmail.com>
Date:   Tue Sep 8 11:47:49 2015 -0400

    webstaff: Use ids for prefix, suffix, label_class
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
index 031581d..235a37a 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
@@ -286,7 +286,7 @@ function(egCore , $q) {
             function ( $scope , itemSvc , egCore ) {
                 $scope.callNumber =  $scope.copies[0].call_number();
 
-                $scope.idTracker = function (x) { if (x) return x.id() };
+                $scope.idTracker = function (x) { if (x && x.id) return x.id() };
 
                 // XXX $() is not working! arg
                 $scope.focusNextBarcode = function (i) {
@@ -315,10 +315,16 @@ function(egCore , $q) {
                 $scope.suffix_list = [];
                 itemSvc.get_suffixes($scope.callNumber.owning_lib()).then(function(list){
                     $scope.suffix_list = list;
+                    $scope.$watch('callNumber.suffix()', function (v) {
+                        $scope.suffix = $scope.suffix_list.filter( function (s) {
+                            return s.id() == v;
+                        })[0];
+                    });
+
                 });
                 $scope.updateSuffix = function () {
                     angular.forEach($scope.copies, function(cp) {
-                        cp.call_number().suffix($scope.suffix);
+                        cp.call_number().suffix($scope.suffix.id());
                         cp.call_number().ischanged(1);
                     });
                 }
@@ -326,10 +332,16 @@ function(egCore , $q) {
                 $scope.prefix_list = [];
                 itemSvc.get_prefixes($scope.callNumber.owning_lib()).then(function(list){
                     $scope.prefix_list = list;
+                    $scope.$watch('callNumber.prefix()', function (v) {
+                        $scope.prefix = $scope.prefix_list.filter(function (p) {
+                            return p.id() == v;
+                        })[0];
+                    });
+
                 });
                 $scope.updatePrefix = function () {
                     angular.forEach($scope.copies, function(cp) {
-                        cp.call_number().prefix($scope.prefix);
+                        cp.call_number().prefix($scope.prefix.id());
                         cp.call_number().ischanged(1);
                     });
                 }
@@ -337,10 +349,16 @@ function(egCore , $q) {
                 $scope.classification_list = [];
                 itemSvc.get_classifications().then(function(list){
                     $scope.classification_list = list;
+                    $scope.$watch('callNumber.label_class()', function (v) {
+                        $scope.classification = $scope.classification_list.filter(function (c) {
+                            return c.id() == v;
+                        })[0];
+                    });
+
                 });
                 $scope.updateClassification = function () {
                     angular.forEach($scope.copies, function(cp) {
-                        cp.call_number().label_class($scope.classification);
+                        cp.call_number().label_class($scope.classification.id());
                         cp.call_number().ischanged(1);
                     });
                 }
@@ -352,33 +370,6 @@ function(egCore , $q) {
                     });
                 }
 
-                $scope.$watch('callNumber.prefix()', function (v) {
-                    if (typeof v != 'object') {
-                        $scope.prefix = $scope.prefix_list.filter(function (p) {
-                            return p.id() == v;
-                        })[0];
-                        $scope.callNumber.prefix($scope.prefix);
-                    }
-                });
-
-                $scope.$watch('callNumber.suffix()', function (v) {
-                    if (typeof v != 'object') {
-                        $scope.suffix = $scope.suffix_list.filter( function (s) {
-                            return s.id() == v;
-                        })[0];
-                        $scope.callNumber.suffix($scope.suffix);
-                    }
-                });
-
-                $scope.$watch('callNumber.label_class()', function (v) {
-                    if (typeof v != 'object') {
-                        $scope.classification = $scope.classification_list.filter(function (c) {
-                            return c.id() == v;
-                        })[0];
-                        $scope.callNumber.label_class($scope.classification);
-                    }
-                });
-
                 $scope.$watch('callNumber.label()', function (v) {
                     $scope.label = v;
                 });
@@ -443,6 +434,13 @@ function(egCore , $q) {
                 $scope.first_cn = Object.keys($scope.struct)[0];
                 $scope.full_cn = $scope.struct[$scope.first_cn][0].call_number();
 
+                $scope.defaults = {};
+                egCore.hatch.getItem('cat.copy.defaults').then(function(t) {
+                    if (t) {
+                        $scope.defaults = t;
+                    }
+                });
+
                 $scope.focusNextFirst = function(prev_cn) {
                     var n;
                     var yep = false;
@@ -489,6 +487,9 @@ function(egCore , $q) {
                             var cn = new egCore.idl.acn();
                             cn.id( --itemSvc.new_cn_id );
                             cn.isnew( true );
+                            cn.prefix( $scope.defaults.prefix || -1 );
+                            cn.suffix( $scope.defaults.suffix || -1 );
+                            cn.label_class( $scope.defaults.classification || 1 );
                             cn.owning_lib( $scope.owning_lib.id() );
                             cn.record( $scope.full_cn.record() );
 
@@ -871,6 +872,9 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
                                 var cn = new egCore.idl.acn();
                                 cn.id( --itemSvc.new_cn_id );
                                 cn.isnew( true );
+                                cn.prefix( $scope.defaults.prefix || -1 );
+                                cn.suffix( $scope.defaults.suffix || -1 );
+                                cn.label_class( $scope.defaults.classification || 1 );
                                 cn.owning_lib( proto.owner || egCore.auth.user().ws_ou() );
                                 cn.record( $scope.record_id );
                                 if (proto.label) cn.label( proto.label );

commit cc3c57a0fa39b681d76a93488927151aaaa89f71
Author: Mike Rylander <mrylander at gmail.com>
Date:   Tue Sep 8 11:47:27 2015 -0400

    webstaff: Protect the cloner from undefined values
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/services/idl.js b/Open-ILS/web/js/ui/default/staff/services/idl.js
index 681a63a..fda58d3 100644
--- a/Open-ILS/web/js/ui/default/staff/services/idl.js
+++ b/Open-ILS/web/js/ui/default/staff/services/idl.js
@@ -23,14 +23,18 @@ angular.module('egCoreMod')
     // Clones data structures containing fieldmapper objects
     service.Clone = function(old) {
         var obj;
-        if (old._isfieldmapper) {
+        if (typeof old == 'undefined') {
+            return old;
+        } else if (old._isfieldmapper) {
             obj = new service[old.classname]()
 
             for( var i in old.a ) {
                 var thing = old.a[i];
                 if(thing === null) continue;
 
-                if( thing._isfieldmapper ) {
+                if (typeof thing == 'undefined') {
+                    obj.a[i] = thing;
+                } else if (thing._isfieldmapper) {
                     obj.a[i] = service.Clone(thing);
                 } else {
 
@@ -39,7 +43,9 @@ angular.module('egCoreMod')
 
                         for( var j in thing ) {
 
-                            if( thing[j]._isfieldmapper )
+                            if (typeof thing[j] == 'undefined')
+                                obj.a[i][j] = thing[j];
+                            else if( thing[j]._isfieldmapper )
                                 obj.a[i][j] = service.Clone(thing[j]);
                             else
                                 obj.a[i][j] = angular.copy(thing[j]);
@@ -53,7 +59,9 @@ angular.module('egCoreMod')
             if(angular.isArray(old)) {
                 obj = [];
                 for( var j in old ) {
-                    if( old[j]._isfieldmapper )
+                    if (typeof old[j] == 'undefined')
+                        obj[j] = old[j];
+                    else if( old[j]._isfieldmapper )
                         obj[j] = service.Clone(old[j]);
                     else
                         obj[j] = angular.copy(old[j]);
@@ -61,7 +69,9 @@ angular.module('egCoreMod')
             } else if(angular.isObject(old)) {
                 obj = {};
                 for( var j in old ) {
-                    if( old[j]._isfieldmapper )
+                    if (typeof old[j] == 'undefined')
+                        obj[j] = old[j];
+                    else if( old[j]._isfieldmapper )
                         obj[j] = service.Clone(old[j]);
                     else
                         obj[j] = angular.copy(old[j]);

commit 417e75f2120f1d31df4c355e5af3f808ef9d7e5e
Author: Mike Rylander <mrylander at gmail.com>
Date:   Fri Sep 4 16:47:43 2015 -0400

    webstaff: MARC editor button layout improvement
    
    Also, add mark-vol-transfer-dest and mark-conjoined-dest
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/share/t_marcedit.tt2 b/Open-ILS/src/templates/staff/cat/share/t_marcedit.tt2
index 9dea0d4..dd6932c 100644
--- a/Open-ILS/src/templates/staff/cat/share/t_marcedit.tt2
+++ b/Open-ILS/src/templates/staff/cat/share/t_marcedit.tt2
@@ -1,5 +1,19 @@
 <div>
   <div ng-show="bre" class="row pad-vert marcfastitemadd" ng-hide="brandNewRecord">
+    <div class="col-md-5">
+      <label>[% l('Mark for:') %]</label>
+      <div class="btn-group">
+        <span class="btn-group">
+          <button class="btn btn-default" ng-click="markOverlay()">[% l('Overlay Target') %]</button>
+        </span>
+        <span class="btn-group">
+          <button class="btn btn-default" ng-click="markVolTransfer()">[% l('Volume Transfer') %]</button>
+        </span>
+        <span class="btn-group">
+          <button class="btn btn-default" ng-click="markConjoined()">[% l('Conjoined Items') %]</button>
+        </span>
+      </div>
+    </div>
     <div class="col-md-2">
       <label><input type="checkbox" ng-model="enable_fast_add"/> [% l('Add Item') %]</label>
     </div>
@@ -27,7 +41,7 @@
     <div ng-if="bre" class="col-md-2">
       <eg-marc-edit-bibsource/>
     </div>
-    <div class="col-md-6">
+    <div class="col-md-3">
       <div class="btn-group">
         <span class="btn-group">
           <button class="btn btn-default" ng-show="record_type == 'bre'" ng-click="validateHeadings()">[% l('Validate') %]</button>
@@ -36,17 +50,11 @@
           <button class="btn btn-default" ng-click="saveRecord()">[% l('Save') %]</button>
         </span>
         <span class="btn-group">
-          <button class="btn btn-default" ng-click="seeBreaker()">[% l('Breaker') %]</button>
-        </span>
-        <span class="btn-group">
           <button ng-hide="brandNewRecord || Record().deleted()" class="btn btn-default" ng-click="deleteRecord()">[% l('Delete') %]</button>
           <button ng-if="!brandNewRecord && Record().deleted()" class="btn btn-default" ng-click="undeleteRecord()">[% l('Undelete') %]</button>
         </span>
       </div>
     </div>
-    <div class="col-md-1" ng-hide="brandNewRecord">
-      <button class="btn btn-default" ng-click="markOverlay()">[% l('Mark as Overlay Target') %]</button>
-    </div>
   </div>
 
   <div ng-show="flatEditor">
diff --git a/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js b/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
index a8df2aa..d081f0c 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
@@ -1110,6 +1110,14 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
                     alert($scope.record.toBreaker());
                 };
 
+                $scope.markConjoined = function () {
+                    egCore.hatch.setLocalItem('eg.cat.marked_conjoined_record',$scope.recordId);
+                };
+
+                $scope.markVolTransfer = function () {
+                    egCore.hatch.setLocalItem('eg.cat.marked_volume_transfer_record',$scope.recordId);
+                };
+
                 $scope.markOverlay = function () {
                     egCore.hatch.setLocalItem('eg.cat.marked_overlay_record',$scope.recordId);
                 };

commit 9f82aee9c55620c2f1e11cff350951b37a5d336c
Author: Jason Etheridge <jason at esilibrary.com>
Date:   Wed Aug 12 13:52:05 2015 -0400

    webstaff: use localStorage for the overlay-record selection
    
    in the Import Record from z39.50 interface
    
    Signed-off-by: Jason Etheridge <jason at esilibrary.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/cat/z3950/app.js b/Open-ILS/web/js/ui/default/staff/cat/z3950/app.js
index 3b39d30..5476c71 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/z3950/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/z3950/app.js
@@ -175,7 +175,7 @@ function($scope , $q , $location , $timeout , $window,  egCore , egGridDataProvi
         return true;
     };
 
-    $scope.local_overlay_target = 0;
+    $scope.local_overlay_target = egCore.hatch.getLocalItem('eg.cat.marked_overlay_record') || 0;
     $scope.mark_as_overlay_target = function() {
         var items = $scope.gridControls.selectedItems();
         if ($scope.local_overlay_target == items[0].tcn()) {
@@ -183,6 +183,7 @@ function($scope , $q , $location , $timeout , $window,  egCore , egGridDataProvi
         } else {
             $scope.local_overlay_target = items[0].tcn();
         }
+        egCore.hatch.setLocalItem('eg.cat.marked_overlay_record',$scope.local_overlay_target);
     }
     $scope.cant_overlay = function() {
         if (!$scope.local_overlay_target) return true;

commit ddc995c9d848c17932b5b32ff976362d634c5476
Author: Jason Etheridge <jason at esilibrary.com>
Date:   Thu Sep 3 01:49:19 2015 -0400

    webstaff: add Mark as Overlay Target to MARC editor
    
    no bells and whistles though :)
    
    Signed-off-by: Jason Etheridge <jason at esilibrary.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/share/t_marcedit.tt2 b/Open-ILS/src/templates/staff/cat/share/t_marcedit.tt2
index eba2c17..9dea0d4 100644
--- a/Open-ILS/src/templates/staff/cat/share/t_marcedit.tt2
+++ b/Open-ILS/src/templates/staff/cat/share/t_marcedit.tt2
@@ -44,6 +44,9 @@
         </span>
       </div>
     </div>
+    <div class="col-md-1" ng-hide="brandNewRecord">
+      <button class="btn btn-default" ng-click="markOverlay()">[% l('Mark as Overlay Target') %]</button>
+    </div>
   </div>
 
   <div ng-show="flatEditor">
diff --git a/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js b/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
index 056c34e..a8df2aa 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
@@ -1110,6 +1110,10 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
                     alert($scope.record.toBreaker());
                 };
 
+                $scope.markOverlay = function () {
+                    egCore.hatch.setLocalItem('eg.cat.marked_overlay_record',$scope.recordId);
+                };
+
                 $scope.$watch('recordId',
                     function(newVal, oldVal) {
                         if (newVal && newVal !== oldVal) {

commit 2e9b5949998f189d044f897b8059a9b411b72f46
Author: Mike Rylander <mrylander at gmail.com>
Date:   Fri Sep 4 15:32:35 2015 -0400

    webstaff: Add confirmation before deleting copies/volumes
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/catalog/index.tt2 b/Open-ILS/src/templates/staff/cat/catalog/index.tt2
index 26c450f..9635c5d 100644
--- a/Open-ILS/src/templates/staff/cat/catalog/index.tt2
+++ b/Open-ILS/src/templates/staff/cat/catalog/index.tt2
@@ -16,6 +16,15 @@
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/circ/services/holds.js"></script>
 [% INCLUDE 'staff/circ/share/hold_strings.tt2' %]
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/cat/catalog/app.js"></script>
+<script>
+  angular.module('egCoreMod').run(['egStrings', function(s) {
+    s.CONFIRM_DELETE_COPIES_VOLUMES =
+      "[% l('Permanently delete selected copies and/or volumes from catalog?') %]";
+    s.CONFIRM_DELETE_COPIES_VOLUMES_MESSAGE =
+      "[% l('Will delete {{copies}} copies and {{volumes}} volumes') %]";
+  }])
+</script>
+
 [% END %]
 
 <div ng-view></div>
diff --git a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
index 1f77a09..4177943 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
@@ -226,9 +226,9 @@ function($scope , $routeParams , $location , $window , $q , egCore) {
 }])
 
 .controller('CatalogCtrl',
-       ['$scope','$routeParams','$location','$window','$q','egCore','egHolds','egCirc',
+       ['$scope','$routeParams','$location','$window','$q','egCore','egHolds','egCirc','egConfirmDialog',
         'egGridDataProvider','egHoldGridActions','$timeout','$modal','holdingsSvc','egUser',
-function($scope , $routeParams , $location , $window , $q , egCore , egHolds , egCirc, 
+function($scope , $routeParams , $location , $window , $q , egCore , egHolds , egCirc,  egConfirmDialog,
          egGridDataProvider , egHoldGridActions , $timeout , $modal , holdingsSvc , egUser) {
 
     // set record ID on page load if available...
@@ -494,15 +494,20 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
         var cnHash = {};
         var perCnCopies = {};
 
+        var cn_count = 0;
+        var cp_count = 0;
+
         angular.forEach(
             $scope.holdingsGridControls.selectedItems(),
             function (item) {
                 if (vols && item.raw_call_number) {
                     cnHash[item.call_number.id] = egCore.idl.Clone(item.raw_call_number);
                     cnHash[item.call_number.id].isdeleted(1);
+                    cn_count++;
                 } else if (copies) {
                     angular.forEach(egCore.idl.Clone(item.raw), function (cp) {
                         cp.isdeleted(1);
+                        cp_count++;
                         var cn_id = cp.call_number().id();
                         if (!cnHash[cn_id]) {
                             cnHash[cn_id] = cp.call_number();
@@ -518,7 +523,10 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
         );
 
         angular.forEach(perCnCopies, function (v, k) {
-            if (vols) cnHash[k].isdeleted(1);
+            if (vols) {
+                cnHash[k].isdeleted(1);
+                cn_count++;
+            }
             cnHash[k].copies(v);
         });
 
@@ -529,12 +537,18 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
 
         if (cnList.length == 0) return;
 
-        egCore.net.request(
-            'open-ils.cat',
-            'open-ils.cat.asset.volume.fleshed.batch.update.override',
-            egCore.auth.token(), cnList, 1, {}
-        ).then(function(update_count) {
-            $scope.holdingsGridDataProvider.refresh();
+        egConfirmDialog.open(
+            egCore.strings.CONFIRM_DELETE_COPIES_VOLUMES,
+            egCore.strings.CONFIRM_DELETE_COPIES_VOLUMES_MESSAGE,
+            {copies : cp_count, volumes : cn_count}
+        ).result.then(function() {
+            egCore.net.request(
+                'open-ils.cat',
+                'open-ils.cat.asset.volume.fleshed.batch.update.override',
+                egCore.auth.token(), cnList, 1, {}
+            ).then(function(update_count) {
+                $scope.holdingsGridDataProvider.refresh();
+            });
         });
     }
     $scope.selectedHoldingsCopyDelete = function () { $scope.selectedHoldingsDelete(false,true) }

commit 1528a147c4f703fff606a93fff23d3b5c2327705
Author: Mike Rylander <mrylander at gmail.com>
Date:   Fri Sep 4 13:59:14 2015 -0400

    webstaff: Allow deleting copys/vols ... need a speed bump?
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2 b/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2
index 154bb4f..c5ab768 100644
--- a/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2
+++ b/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2
@@ -66,6 +66,13 @@
     <eg-grid-action handler="selectedHoldingsVolCopyEdit" group="[% l('Edit') %]"
       label="[% l('Volumes and Copies') %]"></eg-grid-action>
 
+    <eg-grid-action handler="selectedHoldingsEmptyVolCopyDelete" group="[% l('Delete') %]" disabled="vols_not_shown"
+      label="[% l('Empty Volumes') %]"></eg-grid-action>
+    <eg-grid-action handler="selectedHoldingsCopyDelete" group="[% l('Delete') %]" disabled="copies_not_shown"
+      label="[% l('Copies') %]"></eg-grid-action>
+    <eg-grid-action handler="selectedHoldingsVolCopyDelete" group="[% l('Delete') %]" disabled="copies_not_shown"
+      label="[% l('Volumes and Copies') %]"></eg-grid-action>
+
     <eg-grid-action handler="transferVolumes" group="[% l('Transfer') %]"
       label="[% l('Volumes to Previously Marked Library') %]"></eg-grid-action>
     <eg-grid-action handler="transferItems" group="[% l('Transfer') %]"
diff --git a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
index a709f04..1f77a09 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
@@ -451,6 +451,10 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
         return !$scope.holdings_show_vols;
     }
 
+    $scope.copies_not_shown = function () {
+        return !$scope.holdings_show_copies;
+    }
+
     $scope.holdings_checkbox_handler = function (item) {
         $scope.holdings_cb_changed(item.checkbox,item.checked);
     }
@@ -468,7 +472,7 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
         var cp_list = [];
         angular.forEach(
             $scope.holdingsGridControls.selectedItems(),
-            function (item) { cp_list = cp_list.concat(item.raw) }
+            function (item) { if (item.raw) cp_list = cp_list.concat(item.raw) }
         );
         return cp_list;
     }
@@ -485,13 +489,65 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
         return cn_id_list;
     }
 
-    spawnHoldingsAdd = function (hide_vols,hide_copies){
+    $scope.selectedHoldingsDelete = function (vols, copies) {
+
+        var cnHash = {};
+        var perCnCopies = {};
+
+        angular.forEach(
+            $scope.holdingsGridControls.selectedItems(),
+            function (item) {
+                if (vols && item.raw_call_number) {
+                    cnHash[item.call_number.id] = egCore.idl.Clone(item.raw_call_number);
+                    cnHash[item.call_number.id].isdeleted(1);
+                } else if (copies) {
+                    angular.forEach(egCore.idl.Clone(item.raw), function (cp) {
+                        cp.isdeleted(1);
+                        var cn_id = cp.call_number().id();
+                        if (!cnHash[cn_id]) {
+                            cnHash[cn_id] = cp.call_number();
+                            perCnCopies[cn_id] = [cp];
+                        } else {
+                            perCnCopies[cn_id].push(cp);
+                        }
+                        cp.call_number(cn_id); // prevent loops in JSON-ification
+                    });
+
+                }
+            }
+        );
+
+        angular.forEach(perCnCopies, function (v, k) {
+            if (vols) cnHash[k].isdeleted(1);
+            cnHash[k].copies(v);
+        });
+
+        cnList = [];
+        angular.forEach(cnHash, function (v, k) {
+            cnList.push(v);
+        });
+
+        if (cnList.length == 0) return;
+
+        egCore.net.request(
+            'open-ils.cat',
+            'open-ils.cat.asset.volume.fleshed.batch.update.override',
+            egCore.auth.token(), cnList, 1, {}
+        ).then(function(update_count) {
+            $scope.holdingsGridDataProvider.refresh();
+        });
+    }
+    $scope.selectedHoldingsCopyDelete = function () { $scope.selectedHoldingsDelete(false,true) }
+    $scope.selectedHoldingsVolCopyDelete = function () { $scope.selectedHoldingsDelete(true,true) }
+    $scope.selectedHoldingsEmptyVolCopyDelete = function () { $scope.selectedHoldingsDelete(true,false) }
+
+    spawnHoldingsAdd = function (vols,copies){
         var raw = [];
-        if (hide_vols) { // just a copy on existing volumes
+        if (copies) { // just a copy on existing volumes
             angular.forEach(gatherSelectedVolumeIds(), function (v) {
                 raw.push( {callnumber : v} );
             });
-        } else {
+        } else if (vols) {
             angular.forEach(
                 $scope.holdingsGridControls.selectedItems(),
                 function (item) {
@@ -506,8 +562,8 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
             null, 'edit-these-copies', {
                 record_id: $scope.record_id,
                 raw: raw,
-                hide_vols : hide_vols,
-                hide_copies : hide_copies
+                hide_vols : false,
+                hide_copies : false
             }
         ).then(function(key) {
             if (key) {
@@ -518,8 +574,8 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
             }
         });
     }
-    $scope.selectedHoldingsVolCopyAdd = function () { spawnHoldingsAdd(false,false) }
-    $scope.selectedHoldingsCopyAdd = function () { spawnHoldingsAdd(true,false) }
+    $scope.selectedHoldingsVolCopyAdd = function () { spawnHoldingsAdd(true,false) }
+    $scope.selectedHoldingsCopyAdd = function () { spawnHoldingsAdd(false,true) }
 
     spawnHoldingsEdit = function (hide_vols,hide_copies){
         egCore.net.request(
@@ -1018,14 +1074,14 @@ function(egCore , $q) {
                     index = 0;
                     var cp_list = [];
                     var prev_key;
-                    var current_blob = {};
+                    var current_blob = { copy_count : 0 };
                     angular.forEach(new_list, function (cp) {
                         if (!prev_key) {
                             prev_key = cp.owner_list.join('') + cp.call_number.label;
                             if (cp.barcode) current_blob.copy_count = 1;
                             current_blob.index = index++;
                             current_blob.id_list = cp.id_list;
-                            current_blob.raw = cp.raw;
+                            if (cp.raw) current_blob.raw = cp.raw;
                             current_blob.call_number = cp.call_number;
                             current_blob.owner_list = cp.owner_list;
                             current_blob.owner_label = cp.owner_label;
@@ -1040,11 +1096,11 @@ function(egCore , $q) {
                                 current_blob.barcode = current_blob.copy_count;
                                 cp_list.push(current_blob);
                                 prev_key = current_key;
-                                current_blob = {};
+                                current_blob = { copy_count : 0 };
                                 if (cp.barcode) current_blob.copy_count = 1;
                                 current_blob.index = index++;
                                 current_blob.id_list = cp.id_list;
-                                current_blob.raw = cp.raw;
+                                if (cp.raw) current_blob.raw = cp.raw;
                                 current_blob.owner_label = cp.owner_label;
                                 current_blob.owner_id = cp.owner_id;
                                 current_blob.call_number = cp.call_number;
@@ -1062,13 +1118,13 @@ function(egCore , $q) {
                         index = 0;
                         var cn_list = [];
                         prev_key = '';
-                        var current_blob = {};
+                        current_blob = { copy_count : 0 };
                         angular.forEach(cp_list, function (cp) {
                             if (!prev_key) {
                                 prev_key = cp.owner_list.join('');
                                 current_blob.index = index++;
                                 current_blob.id_list = cp.id_list;
-                                current_blob.raw = cp.raw;
+                                if (cp.raw) current_blob.raw = cp.raw;
                                 current_blob.cn_count = 1;
                                 current_blob.copy_count = cp.copy_count;
                                 current_blob.owner_list = cp.owner_list;
@@ -1080,16 +1136,16 @@ function(egCore , $q) {
                                     current_blob.cn_count++;
                                     current_blob.copy_count += cp.copy_count;
                                     current_blob.id_list = current_blob.id_list.concat(cp.id_list);
-                                    current_blob.raw = current_blob.raw.concat(cp.raw);
+                                    if (cp.raw) current_blob.raw = current_blob.raw.concat(cp.raw);
                                 } else {
                                     current_blob.barcode = current_blob.copy_count;
                                     current_blob.call_number = { label : current_blob.cn_count };
                                     cn_list.push(current_blob);
                                     prev_key = current_key;
-                                    current_blob = {};
+                                    current_blob = { copy_count : 0 };
                                     current_blob.index = index++;
                                     current_blob.id_list = cp.id_list;
-                                    current_blob.raw = cp.raw;
+                                    if (cp.raw) current_blob.raw = cp.raw;
                                     current_blob.owner_label = cp.owner_label;
                                     current_blob.owner_id = cp.owner_id;
                                     current_blob.cn_count = 1;
@@ -1116,7 +1172,7 @@ function(egCore , $q) {
             // notify reads the stream of copies, one at a time.
             function(cn) {
 
-                var copies = cn.copies();
+                var copies = cn.copies().filter(function(cp){ return cp.deleted() == 'f' });
                 cn.copies([]);
 
                 angular.forEach(copies, function (cp) {
@@ -1148,7 +1204,8 @@ function(egCore , $q) {
                     service.copies.push({
                         owner_id   : owner_id,
                         owner_list : owner_name_list,
-                        call_number: egCore.idl.toHash(cn)
+                        call_number: egCore.idl.toHash(cn),
+                        raw_call_number: cn
                     });
                 }
 

commit 3e078a7077198058542b8aa71b02f2393081f139
Author: Mike Rylander <mrylander at gmail.com>
Date:   Fri Sep 4 13:57:16 2015 -0400

    webstaff: Add a save-and-continue mode
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2 b/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2
index 4897231..d6805ad 100644
--- a/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2
@@ -113,14 +113,17 @@
                <eg-grid
                  id-field="id"
                  idl-class="acp"
+                 menu-label="[% l('Save... ') %]"
                  features="-pagination,-actions,-index"
                  items-provider="completedGridDataProvider"
                  grid-controls="completedGridControls"
                  persist-key="cat.volcopy.copies.complete">
                
-                 <eg-grid-menu-item handler="completeToWorking"
+                 <eg-grid-menu-item standalone="true" handler="completeToWorking"
                   label="[% l('Edit Selected') %]"></eg-grid-menu-item>
         
+                 <eg-grid-menu-item handler="saveAndContinue"
+                  label="[% l('Save Completed') %]"></eg-grid-menu-item>
                  <eg-grid-menu-item handler="saveAndExit"
                   label="[% l('Save & Exit') %]"></eg-grid-menu-item>
         
diff --git a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
index b0f5dc7..031581d 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
@@ -1117,7 +1117,7 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
         $scope.saveCompletedCopies = function (and_exit) {
             var cnHash = {};
             var perCnCopies = {};
-            angular.forEach( $scope.completed_copies, function (cp) {
+            angular.forEach( egCore.idl.Clone($scope.completed_copies), function (cp) {
                 var cn_id = cp.call_number().id();
                 if (!cnHash[cn_id]) {
                     cnHash[cn_id] = cp.call_number();
@@ -1149,6 +1149,10 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
             });
         }
 
+        $scope.saveAndContinue = function () {
+            $scope.saveCompletedCopies(false);
+        }
+
         $scope.saveAndExit = function () {
             $scope.saveCompletedCopies(true);
         }

commit 043ae87a2015abcd73c1b5da447b2da90598b23d
Author: Mike Rylander <mrylander at gmail.com>
Date:   Fri Sep 4 13:56:33 2015 -0400

    webstaff: Provide a fieldmapper-aware cloning method
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/services/idl.js b/Open-ILS/web/js/ui/default/staff/services/idl.js
index 5934063..681a63a 100644
--- a/Open-ILS/web/js/ui/default/staff/services/idl.js
+++ b/Open-ILS/web/js/ui/default/staff/services/idl.js
@@ -20,6 +20,59 @@ angular.module('egCoreMod')
 
     var service = {};
 
+    // Clones data structures containing fieldmapper objects
+    service.Clone = function(old) {
+        var obj;
+        if (old._isfieldmapper) {
+            obj = new service[old.classname]()
+
+            for( var i in old.a ) {
+                var thing = old.a[i];
+                if(thing === null) continue;
+
+                if( thing._isfieldmapper ) {
+                    obj.a[i] = service.Clone(thing);
+                } else {
+
+                    if(angular.isArray(thing)) {
+                        obj.a[i] = [];
+
+                        for( var j in thing ) {
+
+                            if( thing[j]._isfieldmapper )
+                                obj.a[i][j] = service.Clone(thing[j]);
+                            else
+                                obj.a[i][j] = angular.copy(thing[j]);
+                        }
+                    } else {
+                        obj.a[i] = angular.copy(thing);
+                    }
+                }
+            }
+        } else {
+            if(angular.isArray(old)) {
+                obj = [];
+                for( var j in old ) {
+                    if( old[j]._isfieldmapper )
+                        obj[j] = service.Clone(old[j]);
+                    else
+                        obj[j] = angular.copy(old[j]);
+                }
+            } else if(angular.isObject(old)) {
+                obj = {};
+                for( var j in old ) {
+                    if( old[j]._isfieldmapper )
+                        obj[j] = service.Clone(old[j]);
+                    else
+                        obj[j] = angular.copy(old[j]);
+                }
+            } else {
+                obj = angular.copy(old);
+            }
+        }
+        return obj;
+    };
+
     service.parseIDL = function() {
         //console.debug('egIDL.parseIDL()');
 

commit 5171ee499de09bf08772231e183191d77b66031a
Author: Mike Rylander <mrylander at gmail.com>
Date:   Fri Sep 4 10:56:56 2015 -0400

    webstaff: Make next-barcode-on-enter happy
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2 b/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2
index b067b19..4897231 100644
--- a/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2
@@ -54,7 +54,7 @@
                 </div>
             </div>
         </div> <!-- row -->
-        <eg-vol-edit ng-repeat="(lib,callnumbers) in data.tree" record="record.id()" lib="{{lib}}" allcopies="data.copies" struct="data.tree[lib]"></eg-vol-edit>
+        <eg-vol-edit focus-next="focusNextFirst" ng-repeat="(lib,callnumbers) in data.tree | orderBy:lib track by lib" ng-init="ind = $index" record="record.id()" lib="{{lib}}" allcopies="data.copies" struct="data.tree[lib]"></eg-vol-edit>
     </div>
 
 </div>
diff --git a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
index 0a3bafa..b0f5dc7 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
@@ -194,21 +194,21 @@ function(egCore , $q) {
         template:
             '<div class="row">'+
                 '<div class="col-xs-5">'+
-                    '<input id="{{callNumber.id()}}.{{copy.id()}}"'+
-                    ' eg-enter="nextBarcode()" class="form-control"'+
+                    '<input id="{{callNumber.id()}}_{{copy.id()}}"'+
+                    ' eg-enter="nextBarcode(copy.id())" class="form-control"'+
                     ' type="text" ng-model="barcode" ng-change="updateBarcode()"/>'+
                 '</div>'+
                 '<div class="col-xs-3"><input class="form-control" type="number" ng-model="copy_number" ng-change="updateCopyNo()"/></div>'+
                 '<div class="col-xs-4"><eg-basic-combo-box list="parts" selected="part"></eg-basic-combo-box></div>'+
             '</div>',
 
-        scope: { copy: "=", callNumber: "=", index: "@" },
+        scope: { focusNext: "=", copy: "=", callNumber: "=", index: "@" },
         controller : ['$scope','itemSvc','egCore',
             function ( $scope , itemSvc , egCore ) {
                 $scope.new_part_id = 0;
 
                 $scope.nextBarcode = function (i) {
-                    $scope.$parent.focusNextBarcode($scope.copy.id());
+                    $scope.focusNext(i);
                 }
 
                 $scope.updateBarcode = function () { $scope.copy.barcode($scope.barcode); $scope.copy.ischanged(1); };
@@ -277,11 +277,11 @@ function(egCore , $q) {
                 '</div>'+
                 '<div class="col-xs-1"><input class="form-control" type="number" ng-model="copy_count" min="{{orig_copy_count}}" ng-change="changeCPCount()"></div>'+
                 '<div class="col-xs-5">'+
-                    '<eg-vol-copy-edit ng-repeat="cp in copies track by idTracker(cp)" copy="cp" call-number="callNumber"></eg-vol-copy-edit>'+
+                    '<eg-vol-copy-edit ng-repeat="cp in copies track by idTracker(cp)" focus-next="focusNextBarcode" copy="cp" call-number="callNumber"></eg-vol-copy-edit>'+
                 '</div>'+
             '</div>',
 
-        scope: {allcopies: "=", copies: "=" },
+        scope: {focusNext: "=", allcopies: "=", copies: "=" },
         controller : ['$scope','itemSvc','egCore',
             function ( $scope , itemSvc , egCore ) {
                 $scope.callNumber =  $scope.copies[0].call_number();
@@ -304,9 +304,11 @@ function(egCore , $q) {
                     });
 
                     if (n) {
-                        var next = '#' + $scope.callNumber.id() + '.' + n;
-                        var el = $(next).get(0);
+                        var next = '#' + $scope.callNumber.id() + '_' + n;
+                        var el = $(next);
                         if (el) el.focus()
+                    } else {
+                        $scope.focusNext($scope.callNumber.id())
                     }
                 }
 
@@ -431,16 +433,43 @@ function(egCore , $q) {
                 '<div class="col-xs-1"><eg-org-selector selected="owning_lib" disableTest="cant_have_vols"></eg-org-selector></div>'+
                 '<div class="col-xs-1"><input class="form-control" type="number" min="{{orig_cn_count}}" ng-model="cn_count" ng-change="changeCNCount()"/></div>'+
                 '<div class="col-xs-10">'+
-                    '<eg-vol-row ng-repeat="(cn,copies) in struct track by cn" copies="copies" allcopies="allcopies"></eg-vol-row>'+
+                    '<eg-vol-row ng-repeat="(cn,copies) in struct | orderBy:cn track by cn" focus-next="focusNextFirst" copies="copies" allcopies="allcopies"></eg-vol-row>'+
                 '</div>'+
             '</div>',
 
-        scope: { allcopies: "=", struct: "=", lib: "@", record: "@" },
+        scope: { focusNext: "=", allcopies: "=", struct: "=", lib: "@", record: "@" },
         controller : ['$scope','itemSvc','egCore',
             function ( $scope , itemSvc , egCore ) {
                 $scope.first_cn = Object.keys($scope.struct)[0];
                 $scope.full_cn = $scope.struct[$scope.first_cn][0].call_number();
 
+                $scope.focusNextFirst = function(prev_cn) {
+                    var n;
+                    var yep = false;
+                    angular.forEach(Object.keys($scope.struct).sort(), function (cn) {
+                        console.log('checking '+cn);
+                        if (n) return;
+
+                        if (cn == prev_cn) {
+                            console.log('prev is '+cn);
+                            yep = true;
+                            return;
+                        }
+                        console.log('prev is not '+cn);
+
+                        if (yep) n = cn;
+                    });
+
+                    console.log('found '+n);
+                    if (n) {
+                        var next = '#' + n + '_' + $scope.struct[n][0].id();
+                        var el = $(next);
+                        if (el) el.focus()
+                    } else {
+                        $scope.focusNext($scope.lib);
+                    }
+                }
+
                 $scope.cn_count = Object.keys($scope.struct).length;
                 $scope.orig_cn_count = $scope.cn_count;
 
@@ -476,10 +505,11 @@ function(egCore , $q) {
                         var how_many = o - n;
                         var list = Object
                                 .keys($scope.struct)
-                                .sort(function(a, b){return a-b})
-                                .reverse();
-                        for (var i = how_many; i > 0; i--) {
+                                .sort(function(a, b){return parseInt(a)-parseInt(b)})
+                                .filter(function(x){ return parseInt(x) <= 0 });
+                        for (var i = 0; i < how_many; i++) {
                             // Trimming the global list is a bit more tricky
+                            console.log('trying to trim ' + i);
                             angular.forEach($scope.struct[list[i]], function (d) {
                                 angular.forEach( $scope.allcopies, function (l, j) { 
                                     if (l === d) $scope.allcopies.splice(j,1);
@@ -867,6 +897,34 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
             $scope.workingGridDataProvider.refresh();
         });
 
+        $scope.focusNextFirst = function(prev_lib) {
+            var n;
+            var yep = false;
+            angular.forEach(Object.keys($scope.data.tree).sort(), function (lib) {
+                console.log('checking lib '+lib);
+                if (n) return;
+
+                if (lib == prev_lib) {
+                    console.log('prev is '+lib);
+                    yep = true;
+                    return;
+                }
+                console.log('prev is not '+lib);
+
+                if (yep) n = lib;
+            });
+
+            console.log('found '+n);
+            if (n) {
+                var first_cn = Object.keys($scope.data.tree[n])[0];
+                var next = '#' + first_cn + '_' + $scope.data.tree[n][first_cn][0].id();
+                var el = $(next);
+                if (el) el.focus()
+            } else {
+                $scope.focusNext($scope.lib);
+            }
+        }
+
         $scope.in_item_select = false;
         $scope.afterItemSelect = function() { $scope.in_item_select = false };
         $scope.handleItemSelect = function (item_list) {
@@ -955,7 +1013,7 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
 
                 var final_orgs = all_orgs.filter(function(e,i,a){
                     return a.lastIndexOf(e) === i;
-                }).sort(function(a,b){return b-a});
+                }).sort(function(a, b){return parseInt(a)-parseInt(b)});
 
                 if ($scope.location_orgs.toString() != final_orgs.toString()) {
                     $scope.location_orgs = final_orgs;

commit bc2713b733da00427932d1be1a562349dccdd945
Author: Mike Rylander <mrylander at gmail.com>
Date:   Fri Sep 4 08:47:03 2015 -0400

    webstaff: Enable Fast Item Add functionality in the MARC editor, and improve button layout
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/share/t_marcedit.tt2 b/Open-ILS/src/templates/staff/cat/share/t_marcedit.tt2
index eef0f6a..eba2c17 100644
--- a/Open-ILS/src/templates/staff/cat/share/t_marcedit.tt2
+++ b/Open-ILS/src/templates/staff/cat/share/t_marcedit.tt2
@@ -1,34 +1,48 @@
 <div>
-  <div ng-if="bre" class="row col-md-12 pad-vert marcfastitemadd" ng-hide="brandNewRecord">
-    <input id="mfiacn" type="text" placeholder="[% l('Call Number') %]" ng-model="fast_item_callnumber"/>
-    <input id="mfiabc" type="text" placeholder="[% l('Barcode') %]" ng-model="fast_item_barcode"/>
-    <button class="btn btn-default" ng-click="saveFastItem()">Add Item</button>
+  <div ng-show="bre" class="row pad-vert marcfastitemadd" ng-hide="brandNewRecord">
+    <div class="col-md-2">
+      <label><input type="checkbox" ng-model="enable_fast_add"/> [% l('Add Item') %]</label>
+    </div>
+    <div class="col-md-2">
+      <input id="mfiacn" class="form-control" ng-show="enable_fast_add" type="text" placeholder="[% l('Call Number') %]" ng-model="fast_item_callnumber"/>
+    </div>
+    <div class="col-md-2">
+      <input id="mfiabc" class="form-control" ng-show="enable_fast_add" type="text" placeholder="[% l('Barcode') %]" ng-model="fast_item_barcode"/>
+    </div>
   </div>
 
-  <div class="pad-vert row col-md-12 marctypesource">
+  <div class="pad-vert row marctypesource">
     <div class="col-md-2">
-      <label>Flat Text Editor:</label>
-      <input type="checkbox" ng-model="flatEditor" ng-change="refreshVisual()"/>
+      <label>
+        <input type="checkbox" ng-model="flatEditor" ng-change="refreshVisual()"/>
+        [% l('Flat Text Editor') %]
+      </label>
     </div>
     <div class="col-md-2">
-      <label>Record Type:</label>
-      {{calculated_record_type}}
+      <div class="input-group">
+        <span class="input-group-addon"><b>[% l('Record Type') %]</b></span>
+        <span class="input-group-addon">{{calculated_record_type}}</span>
+      </div>
     </div>
     <div ng-if="bre" class="col-md-2">
       <eg-marc-edit-bibsource/>
     </div>
-    <div class="col-md-1">
-      <button class="btn btn-default" ng-show="record_type == 'bre'" ng-click="validateHeadings()">[% l('Validate') %]</button>
-    </div>
-    <div class="col-md-1">
-      <button class="btn btn-default" ng-click="saveRecord()">Save</button>
-    </div>
-    <div class="col-md-1">
-      <button class="btn btn-default" ng-click="seeBreaker()">Breaker</button>
-    </div>
-    <div class="col-md-1" ng-hide="brandNewRecord">
-      <button ng-hide="Record().deleted()" class="btn btn-default" ng-click="deleteRecord()">Delete</button>
-      <button ng-show="Record().deleted()" class="btn btn-default" ng-click="undeleteRecord()">Undelete</button>
+    <div class="col-md-6">
+      <div class="btn-group">
+        <span class="btn-group">
+          <button class="btn btn-default" ng-show="record_type == 'bre'" ng-click="validateHeadings()">[% l('Validate') %]</button>
+        </span>
+        <span class="btn-group">
+          <button class="btn btn-default" ng-click="saveRecord()">[% l('Save') %]</button>
+        </span>
+        <span class="btn-group">
+          <button class="btn btn-default" ng-click="seeBreaker()">[% l('Breaker') %]</button>
+        </span>
+        <span class="btn-group">
+          <button ng-hide="brandNewRecord || Record().deleted()" class="btn btn-default" ng-click="deleteRecord()">[% l('Delete') %]</button>
+          <button ng-if="!brandNewRecord && Record().deleted()" class="btn btn-default" ng-click="undeleteRecord()">[% l('Undelete') %]</button>
+        </span>
+      </div>
     </div>
   </div>
 
diff --git a/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js b/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
index 119644d..056c34e 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
@@ -502,11 +502,14 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
             });
 
         },
-        controller : ['$timeout','$scope','$q','egCore', 'egTagTable',
-            function ( $timeout , $scope , $q,  egCore ,  egTagTable ) {
+        controller : ['$timeout','$scope','$q','$window','egCore', 'egTagTable',
+            function ( $timeout , $scope , $q,  $window , egCore ,  egTagTable ) {
 
                 MARC21.Record.delimiter = '$';
 
+                $scope.enable_fast_add = false;
+                $scope.fast_item_callnumber = '';
+                $scope.fast_item_barcode = '';
                 $scope.flatEditor = false;
                 $scope.brandNewRecord = false;
                 $scope.bib_source = null;
@@ -1044,7 +1047,30 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
                     if ($scope.recordId) {
                         return egCore.pcrud.update(
                             $scope.Record()
-                        ).then(loadRecord);
+                        ).then(function() {
+                            if ($scope.enable_fast_add) {
+                                egCore.net.request(
+                                    'open-ils.actor',
+                                    'open-ils.actor.anon_cache.set_value',
+                                    null, 'edit-these-copies', {
+                                        record_id: $scope.recordId,
+                                        raw: [{
+                                            label : $scope.fast_item_callnumber,
+                                            barcode : $scope.fast_item_barcode,
+                                        }],
+                                        hide_vols : false,
+                                        hide_copies : false
+                                    }
+                                ).then(function(key) {
+                                    if (key) {
+                                        var url = egCore.env.basePath + 'cat/volcopy/' + key;
+                                        $timeout(function() { $window.open(url, '_blank') });
+                                    } else {
+                                        alert('Could not create anonymous cache key!');
+                                    }
+                                });
+                            }
+                        }).then(loadRecord);
                     } else {
                         $scope.Record().creator(egCore.auth.user().id());
                         $scope.Record().create_date('now');
@@ -1052,8 +1078,32 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
                             $scope.Record()
                         ).then(function(bre) {
                             $scope.recordId = bre.id(); 
+                            if ($scope.enable_fast_add) {
+                                egCore.net.request(
+                                    'open-ils.actor',
+                                    'open-ils.actor.anon_cache.set_value',
+                                    null, 'edit-these-copies', {
+                                        record_id: $scope.recordId,
+                                        raw: [{
+                                            label : $scope.fast_item_callnumber,
+                                            barcode : $scope.fast_item_barcode,
+                                        }],
+                                        hide_vols : false,
+                                        hide_copies : false
+                                    }
+                                ).then(function(key) {
+                                    if (key) {
+                                        var url = egCore.env.basePath + 'cat/volcopy/' + key;
+                                        $timeout(function() { $window.open(url, '_blank') });
+                                    } else {
+                                        alert('Could not create anonymous cache key!');
+                                    }
+                                });
+                            }
                         }).then(loadRecord);
                     }
+
+
                 };
 
                 $scope.seeBreaker = function () {
diff --git a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
index 8cd4b78..0a3bafa 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
@@ -809,6 +809,7 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
                     return itemSvc.fetchIds(data.copies);
 
                 if (data.raw && data.raw.length) {
+                    $scope.dirty = true;
 
                     /* data.raw data structure looks like this:
                      * [{

commit 3e08b6467615261cb194c8f5573867f4e6c7b55f
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Thu Sep 3 21:47:25 2015 +0000

    webstaff: add additional FF definitions for authority records
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
index e8bf7c1..9c44f82 100644
--- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql
+++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
@@ -6135,6 +6135,26 @@ INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, leng
 INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('Type', 'ldr', 'VIS', 6, 1, 'g');
 INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('Subj', '008', 'AUT', 11, 1, '|');
 INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('RecStat', 'ldr', 'AUT', 5, 1, 'n');
+INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('Type', 'ldr', 'AUT', 6, 1, 'z');
+INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('GeoDiv', '008', 'AUT', 6, 1, ' ');
+INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('Roman', '008', 'AUT', 7, 1, ' ');
+INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('CatLang', '008', 'AUT', 8, 1, ' ');
+INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('Kind', '008', 'AUT', 9, 1, ' ');
+INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('Rules', '008', 'AUT', 10, 1, ' ');
+INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('Subj', '008', 'AUT', 11, 1, ' ');
+INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('Series', '008', 'AUT', 12, 1, ' ');
+INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('SerNum', '008', 'AUT', 13, 1, ' ');
+INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('NameUse', '008', 'AUT', 14, 1, ' ');
+INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('SubjUse', '008', 'AUT', 15, 1, ' ');
+INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('SerUse', '008', 'AUT', 16, 1, ' ');
+INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('TypeSubd', '008', 'AUT', 17, 1, ' ');
+INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('GovtAgn', '008', 'AUT', 28, 1, ' ');
+INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('RefStatus', '008', 'AUT', 29, 1, ' ');
+INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('UpdStatus', '008', 'AUT', 31, 1, ' ');
+INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('Name', '008', 'AUT', 32, 1, ' ');
+INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('Status', '008', 'AUT', 33, 1, ' ');
+INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('ModRec', '008', 'AUT', 38, 1, ' ');
+INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('Source', '008', 'AUT', 39, 1, ' ');
 
 INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('File', '008', 'COM', 26, 1, 'u');
 INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('File', '006', 'COM', 9, 1, 'u');
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.additional_authority_fixed_fields.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.additional_authority_fixed_fields.sql
new file mode 100644
index 0000000..2e220bd
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.additional_authority_fixed_fields.sql
@@ -0,0 +1,26 @@
+BEGIN;
+
+-- SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+
+INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('Type', 'ldr', 'AUT', 6, 1, 'z');
+INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('GeoDiv', '008', 'AUT', 6, 1, ' ');
+INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('Roman', '008', 'AUT', 7, 1, ' ');
+INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('CatLang', '008', 'AUT', 8, 1, ' ');
+INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('Kind', '008', 'AUT', 9, 1, ' ');
+INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('Rules', '008', 'AUT', 10, 1, ' ');
+INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('Subj', '008', 'AUT', 11, 1, ' ');
+INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('Series', '008', 'AUT', 12, 1, ' ');
+INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('SerNum', '008', 'AUT', 13, 1, ' ');
+INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('NameUse', '008', 'AUT', 14, 1, ' ');
+INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('SubjUse', '008', 'AUT', 15, 1, ' ');
+INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('SerUse', '008', 'AUT', 16, 1, ' ');
+INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('TypeSubd', '008', 'AUT', 17, 1, ' ');
+INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('GovtAgn', '008', 'AUT', 28, 1, ' ');
+INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('RefStatus', '008', 'AUT', 29, 1, ' ');
+INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('UpdStatus', '008', 'AUT', 31, 1, ' ');
+INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('Name', '008', 'AUT', 32, 1, ' ');
+INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('Status', '008', 'AUT', 33, 1, ' ');
+INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('ModRec', '008', 'AUT', 38, 1, ' ');
+INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, length, default_val) VALUES ('Source', '008', 'AUT', 39, 1, ' ');
+
+COMMIT;

commit 13d664a679e7af33129e6600887cb32c3adf3134
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Thu Sep 3 21:27:25 2015 +0000

    webstaff: put heading validation lookups in a promise chain
    
    Doing this to avoid spamming potentially a couple dozen
    authority record lookups all at once.
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js b/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
index 9d11cd7..119644d 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
@@ -995,6 +995,7 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
 
                 $scope.validateHeadings = function () {
                     if ($scope.record_type != 'bre') return;
+                    var chain = $q.when();
                     angular.forEach($scope.record.fields, function(f) {
                         if (!$scope.controlSet.bibFieldByTag(f.tag)) return;
                         // if heading already has a $0, assume it's good
@@ -1004,25 +1005,28 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
                             return;
                         }
                         var auth_match = $scope.controlSet.bibToAuthorities(f);
-                        egCore.net.request(
-                            'open-ils.search',
-                            'open-ils.search.authority.simple_heading.from_xml.batch.atomic',
-                            auth_match[0]
-                        ).then(function (matches) {
-                            f.heading_valid = false;
-                            if (matches[0]) { // probably set
-                                for (var cset in matches[0]) {
-                                    var arr = matches[0][cset];
-                                    if (arr.length) {
-                                        // protect against errant empty string values
-                                        if (arr.length == 1 && arr[0] == '')
-                                            continue;
-                                        f.heading_valid = true;
-                                        break;
+                        chain = chain.then(function() {
+                            var promise = egCore.net.request(
+                                'open-ils.search',
+                                'open-ils.search.authority.simple_heading.from_xml.batch.atomic',
+                                auth_match[0]
+                            ).then(function (matches) {
+                                f.heading_valid = false;
+                                if (matches[0]) { // probably set
+                                    for (var cset in matches[0]) {
+                                        var arr = matches[0][cset];
+                                        if (arr.length) {
+                                            // protect against errant empty string values
+                                            if (arr.length == 1 && arr[0] == '')
+                                                continue;
+                                            f.heading_valid = true;
+                                            break;
+                                        }
                                     }
                                 }
-                            }
-                            f.heading_checked = true;
+                                f.heading_checked = true;
+                            });
+                            return promise;
                         });
                     });
                 }

commit 6970735361eecb13327d61ac3437e99fbac128cd
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Thu Sep 3 21:13:27 2015 +0000

    webstaff: improve performance of open-ils.search.authority.simple_heading.from_xml
    
    Searching authority.record_entry by comparing are.simple_heading
    with the results of authority.simple_normalize_heading() can result
    in bad query plans; if we calculate the results of that function
    first, we have a much better chance of hitting the index on
    simple_heading.
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Authority.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Authority.pm
index d1f42d7..a20dfb1 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Authority.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Authority.pm
@@ -66,19 +66,26 @@ sub search_authority_by_simple_normalize_heading {
     my $marcxml = shift;
     my $controlset = shift;
 
+    my $norm_heading_query = {
+        from => [ 'authority.simple_normalize_heading' => $marcxml ]
+    };
+
+    my $e = new_editor();
+    my $norm_heading = $e->json_query($norm_heading_query)->[0]->{'authority.simple_normalize_heading'};
+
     my $query = {
         select => { are => ['id'] },
         from   => 'are',
         where  => {
             deleted => 'f',
             simple_heading => {
-                'startwith' => [ 'authority.simple_normalize_heading' => $marcxml ]
+                'startwith' => $norm_heading
             },
             defined($controlset) ? ( control_set => $controlset ) : ()
         }
     };
 
-    $client->respond($_->{id}) for @{ new_editor()->json_query( $query ) };
+    $client->respond($_->{id}) for @{ $e->json_query( $query ) };
     $client->respond_complete;
 }
 __PACKAGE__->register_method(

commit 208ac82a84dae508952183e58e394cf877bf1b46
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Thu Sep 3 20:26:24 2015 +0000

    webstaff: add Validate (headings) button to MARC editor
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/share/t_marcedit.tt2 b/Open-ILS/src/templates/staff/cat/share/t_marcedit.tt2
index f450316..eef0f6a 100644
--- a/Open-ILS/src/templates/staff/cat/share/t_marcedit.tt2
+++ b/Open-ILS/src/templates/staff/cat/share/t_marcedit.tt2
@@ -18,6 +18,9 @@
       <eg-marc-edit-bibsource/>
     </div>
     <div class="col-md-1">
+      <button class="btn btn-default" ng-show="record_type == 'bre'" ng-click="validateHeadings()">[% l('Validate') %]</button>
+    </div>
+    <div class="col-md-1">
       <button class="btn btn-default" ng-click="saveRecord()">Save</button>
     </div>
     <div class="col-md-1">
diff --git a/Open-ILS/src/templates/staff/css/cat.css.tt2 b/Open-ILS/src/templates/staff/css/cat.css.tt2
index fcd98bd..f3f27d2 100644
--- a/Open-ILS/src/templates/staff/css/cat.css.tt2
+++ b/Open-ILS/src/templates/staff/css/cat.css.tt2
@@ -43,6 +43,10 @@ input.marcedit:focus {
     border-right: 0px !important;
 }
 
+.unvalidatedheading {
+    color: red;
+}
+
 .marctag, .marcind {
     text-align: center;
 }
diff --git a/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js b/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
index ca161f5..9d11cd7 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
@@ -363,7 +363,7 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
                     '<span><eg-marc-edit-tag field="field" tag="field.tag" on-keydown="onKeydown"/></span>'+
                     '<span><eg-marc-edit-ind field="field" ind="field.ind1" on-keydown="onKeydown" ind-number="1"/></span>'+
                     '<span><eg-marc-edit-ind field="field" ind="field.ind2" on-keydown="onKeydown" ind-number="2"/></span>'+
-                    '<span><eg-marc-edit-subfield ng-repeat="subfield in field.subfields" subfield="subfield" field="field" on-keydown="onKeydown"/></span>'+
+                    '<span><eg-marc-edit-subfield ng-class="{ \'unvalidatedheading\' : field.heading_checked && !field.heading_valid}" ng-repeat="subfield in field.subfields" subfield="subfield" field="field" on-keydown="onKeydown"/></span>'+
                     // FIXME: template should probably be moved to file to improve
                     // translatibility
                     '<button class="btn btn-info btn-xs" '+
@@ -373,6 +373,8 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
                     '>'+
                     '<span class="glyphicon glyphicon-link"></span>'+
                     '</button>'+
+                    '<span ng-show="field.heading_checked && field.heading_valid" class="glyphicon glyphicon-ok-sign"></span>'+
+                    '<span ng-show="field.heading_checked && !field.heading_valid" class="glyphicon glyphicon-question-sign"></span>'+
                   '</div>',
         scope: { field: "=", onKeydown: '=' },
         replace: true,
@@ -991,6 +993,40 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
                     return $scope.saveRecord();
                 };
 
+                $scope.validateHeadings = function () {
+                    if ($scope.record_type != 'bre') return;
+                    angular.forEach($scope.record.fields, function(f) {
+                        if (!$scope.controlSet.bibFieldByTag(f.tag)) return;
+                        // if heading already has a $0, assume it's good
+                        if (f.subfield('0', true).length) {
+                            f.heading_checked = true;
+                            f.heading_valid = true;
+                            return;
+                        }
+                        var auth_match = $scope.controlSet.bibToAuthorities(f);
+                        egCore.net.request(
+                            'open-ils.search',
+                            'open-ils.search.authority.simple_heading.from_xml.batch.atomic',
+                            auth_match[0]
+                        ).then(function (matches) {
+                            f.heading_valid = false;
+                            if (matches[0]) { // probably set
+                                for (var cset in matches[0]) {
+                                    var arr = matches[0][cset];
+                                    if (arr.length) {
+                                        // protect against errant empty string values
+                                        if (arr.length == 1 && arr[0] == '')
+                                            continue;
+                                        f.heading_valid = true;
+                                        break;
+                                    }
+                                }
+                            }
+                            f.heading_checked = true;
+                        });
+                    });
+                }
+
                 $scope.saveRecord = function () {
                     if ($scope.inPlaceMode) {
                         $scope.marcXml = $scope.record.toXmlString();
diff --git a/Open-ILS/web/js/ui/default/staff/cat/services/tagtable.js b/Open-ILS/web/js/ui/default/staff/cat/services/tagtable.js
index 1580ca5..6c3e10c 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/services/tagtable.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/services/tagtable.js
@@ -489,16 +489,6 @@ function($q,   egCore,   egAuth) {
             return auth_list;
         }
     
-        // This should not be used in an angular world.  Instead, the call
-        // to open-ils.search.authority.simple_heading.from_xml.batch.atomic should
-        // be performed by the code that wants to find matching authorities.
-        this.findMatchingAuthorities = function (field) {
-            return fieldmapper.standardRequest(
-                [ 'open-ils.search', 'open-ils.search.authority.simple_heading.from_xml.batch.atomic' ],
-                this.bibToAuthorities(field)
-            );
-        }
-
     }
 
     service.getAuthorityControlSet = function() {

commit 1e61a8e9e1bc9b32f1d2fa502a376c54e6681c0f
Author: Mike Rylander <mrylander at gmail.com>
Date:   Thu Sep 3 14:24:16 2015 -0400

    webstaff: Add Request Items to copy buckets
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/bucket/copy/t_view.tt2 b/Open-ILS/src/templates/staff/cat/bucket/copy/t_view.tt2
index a226dca..420dd80 100644
--- a/Open-ILS/src/templates/staff/cat/bucket/copy/t_view.tt2
+++ b/Open-ILS/src/templates/staff/cat/bucket/copy/t_view.tt2
@@ -10,6 +10,8 @@
 
   [% INCLUDE 'staff/cat/bucket/copy/t_grid_menu.tt2' %]
 
+  <eg-grid-action label="[% l('Request Selected Copies') %]" 
+    handler="requestItems"></eg-grid-action>
   <eg-grid-action label="[% l('Edit Selected Copies') %]" 
     handler="spawnHoldingsEdit"></eg-grid-action>
   <eg-grid-action label="[% l('Remove Selected Copies') %]" 
diff --git a/Open-ILS/web/js/ui/default/staff/cat/bucket/copy/app.js b/Open-ILS/web/js/ui/default/staff/cat/bucket/copy/app.js
index 4291211..c866a65 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/bucket/copy/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/bucket/copy/app.js
@@ -13,7 +13,7 @@
  */
 
 angular.module('egCatCopyBuckets', 
-    ['ngRoute', 'ui.bootstrap', 'egCoreMod', 'egUiMod', 'egGridMod'])
+    ['ngRoute', 'ui.bootstrap', 'egCoreMod', 'egUiMod', 'egGridMod', 'egUserMod'])
 
 .config(function($routeProvider, $locationProvider, $compileProvider) {
     $locationProvider.html5Mode(true);
@@ -401,9 +401,9 @@ function($scope,  $routeParams,  bucketSvc , egGridDataProvider,   egCore) {
 }])
 
 .controller('ViewCtrl',
-       ['$scope','$q','$routeParams','$timeout','$window','bucketSvc','egCore',
+       ['$scope','$q','$routeParams','$timeout','$window','$modal','bucketSvc','egCore','egUser',
         'egConfirmDialog',
-function($scope,  $q , $routeParams , $timeout , $window , bucketSvc , egCore,
+function($scope,  $q , $routeParams , $timeout , $window , $modal , bucketSvc , egCore , egUser ,
          egConfirmDialog) {
 
     $scope.setTab('view');
@@ -477,6 +477,80 @@ function($scope,  $q , $routeParams , $timeout , $window , bucketSvc , egCore,
         });
     }
 
+    $scope.requestItems = function() {
+        var copy_list = $scope.gridControls.selectedItems().map(
+            function (i) {
+                i.id;
+            }
+        );
+
+        if (copy_list.length == 0) return;
+
+        return $modal.open({
+            templateUrl: './cat/catalog/t_request_items',
+            animation: true,
+            controller:
+                   ['$scope','$modalInstance',
+            function($scope , $modalInstance) {
+                $scope.user = null;
+                $scope.first_user_fetch = true;
+
+                $scope.hold_data = {
+                    hold_type : 'C',
+                    copy_list : copy_list,
+                    pickup_lib: egCore.org.get(egCore.auth.user().ws_ou()),
+                    user      : egCore.auth.user().id()
+                };
+
+                egUser.get( $scope.hold_data.user ).then(function(u) {
+                    $scope.user = u;
+                    $scope.barcode = u.card().barcode();
+                    $scope.user_name = egUser.format_name(u);
+                    $scope.hold_data.user = u.id();
+                });
+
+                $scope.user_name = '';
+                $scope.barcode = '';
+                $scope.$watch('barcode', function (n) {
+                    if (!$scope.first_user_fetch) {
+                        egUser.getByBarcode(n).then(function(u) {
+                            $scope.user = u;
+                            $scope.user_name = egUser.format_name(u);
+                            $scope.hold_data.user = u.id();
+                        }, function() {
+                            $scope.user = null;
+                            $scope.user_name = '';
+                            delete $scope.hold_data.user;
+                        });
+                    }
+                    $scope.first_user_fetch = false;
+                });
+
+                $scope.ok = function(h) {
+                    var args = {
+                        patronid  : h.user,
+                        hold_type : h.hold_type,
+                        pickup_lib: h.pickup_lib.id(),
+                        depth     : 0
+                    };
+
+                    egCore.net.request(
+                        'open-ils.circ',
+                        'open-ils.circ.holds.test_and_create.batch.override',
+                        egCore.auth.token(), args, h.copy_list
+                    );
+
+                    $modalInstance.close();
+                }
+
+                $scope.cancel = function($event) {
+                    $modalInstance.dismiss();
+                    $event.preventDefault();
+                }
+            }]
+        });
+    }
+
     $scope.deleteCopiesFromCatalog = function(copies) {
         egConfirmDialog.open(
             egCore.strings.CONFIRM_DELETE_COPY_BUCKET_ITEMS_FROM_CATALOG,

commit 564ed056e6dc77809ebb720c0f0d0040e904c7e0
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Thu Sep 3 17:17:59 2015 +0000

    webstaff: prune empty fields and subfields before saving MARC record
    
    This implements functionality that was handled server-side
    by the XUL staff client.
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js b/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
index 58ec75b..ca161f5 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
@@ -999,6 +999,7 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
                     $scope.mangle_005();
                     $scope.Record().editor(egCore.auth.user().id());
                     $scope.Record().edit_date('now');
+                    $scope.record.pruneEmptyFieldsAndSubfields();
                     $scope.Record().marc($scope.record.toXmlString());
                     if ($scope.recordId) {
                         return egCore.pcrud.update(
diff --git a/Open-ILS/web/js/ui/default/staff/marcrecord.js b/Open-ILS/web/js/ui/default/staff/marcrecord.js
index 2f03706..95318ba 100644
--- a/Open-ILS/web/js/ui/default/staff/marcrecord.js
+++ b/Open-ILS/web/js/ui/default/staff/marcrecord.js
@@ -335,6 +335,27 @@ var MARC21 = {
             return this;
         }
 
+        this.pruneEmptyFieldsAndSubfields = function() {
+            var me = this;
+            var fields_to_remove = [];
+            for (var i = 0; i < this.fields.length; i++) {
+                var f = this.fields[i];
+                if (f.isControlfield()) {
+                    if (!f.data){
+                        fields_to_remove.push(f);
+                    }
+                } else {
+                    f.pruneEmptySubfields();
+                    if (f.isEmptyDatafield()) {
+                        fields_to_remove.push(f);
+                    }
+                }
+            }
+            fields_to_remove.forEach(function(f) {
+                me.deleteField(f);
+            });
+        }
+
         this.toBreaker = function () {
 
             var me = this;
@@ -658,6 +679,30 @@ var MARC21 = {
             return this.tag < '010' ? true : false;
         }
 
+        this.pruneEmptySubfields = function () {
+            if (this.isControlfield()) return;
+            var me = this;
+            var subfields_to_remove = [];
+            this.subfields.forEach( function(f) {
+                if (f[1] == '') {
+                    subfields_to_remove.push(f);
+                }
+            });
+            subfields_to_remove.forEach(function(f) {
+                me.deleteExactSubfields(f);
+            });
+        }
+        this.isEmptyDatafield = function () {
+            if (this.isControlfield()) return false;
+            var isEmpty = true;
+            this.subfields.forEach( function(f) {
+                if (isEmpty && f[1] != '') {
+                    isEmpty = false;
+                }
+            });
+            return isEmpty;
+        }
+
         this.indicator = function (num, value) {
             if (value !== undefined) {
                 if (num == 1) this.ind1 = value;

commit 44e8fc74d8d56fc0623129113a8dfd3acfb48e5b
Author: Mike Rylander <mrylander at gmail.com>
Date:   Thu Sep 3 12:42:33 2015 -0400

    webstaff: Add modal "request items" to holding view
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/base_js.tt2 b/Open-ILS/src/templates/staff/base_js.tt2
index 5e60ee0..97a2ce0 100644
--- a/Open-ILS/src/templates/staff/base_js.tt2
+++ b/Open-ILS/src/templates/staff/base_js.tt2
@@ -28,6 +28,7 @@
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/services/hatch.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/services/print.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/services/coresvc.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/user.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/services/navbar.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/services/statusbar.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/services/ui.js"></script>
diff --git a/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2 b/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2
index 5ef56d1..154bb4f 100644
--- a/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2
+++ b/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2
@@ -33,6 +33,9 @@
       checkbox="holdings_show_vols"
       checked="holdings_show_vols"/>
 
+    <eg-grid-action handler="requestItems"
+      label="[% l('Request Items') %]"></eg-grid-action>
+
     <eg-grid-action handler="selectedHoldingsItemStatus" group="[% l('Show') %]"
       label="[% l('Item Status (list)') %]"></eg-grid-action>
     <eg-grid-action handler="selectedHoldingsItemStatusDetail" group="[% l('Show') %]"
diff --git a/Open-ILS/src/templates/staff/cat/catalog/t_request_items.tt2 b/Open-ILS/src/templates/staff/cat/catalog/t_request_items.tt2
new file mode 100644
index 0000000..f28a521
--- /dev/null
+++ b/Open-ILS/src/templates/staff/cat/catalog/t_request_items.tt2
@@ -0,0 +1,48 @@
+<form ng-submit="ok(hold_data)" role="form">
+  <div class="modal-header">
+    <button type="button" class="close" ng-click="cancel()" 
+      aria-hidden="true">×</button>
+    <h4 class="modal-title">[% l('Request Items') %]</h4>
+  </div>
+  <div class="modal-body">
+    <div class="row">
+      <div class="col-md-6">
+        <div class="input-group">
+          <span class="input-group-addon">[% l('User Barcode') %]</span>
+          <input class="form-control" type="text" focus-me="true"
+            ng-model="barcode" required placeholder="[% l('User Barcode') %]"/>
+        </div>
+      </div>
+      <div class="col-md-6"> {{user_name}} </div>
+    </div>
+    <div class="row pad-vert">
+      <div class="col-md-6">
+        <div class="input-group">
+          <span class="input-group-addon">[% l('Hold Type') %]</span>
+          <select class="form-control" required ng-model="hold_data.hold_type">
+            <option value="C">[% l('Copy Hold') %]</option>
+            <option value="R" selected>[% l('Recall Hold') %]</option>
+            <option value="F">[% l('Force Hold') %]</option>
+          </select>
+        </div>
+      </div>
+      <div class="col-md-6">
+        <div class="input-group">
+          <span class="input-group-addon">[% l('Pickup Lib') %]</span>
+          <eg-org-selector selected="hold_data.pickup_lib" disableTest="cant_have_vols"></eg-org-selector>
+        </div>
+      </div>
+    </div>
+  </div>
+  <div class="modal-footer">
+    <div class="row">
+      <div class="col-md-6">
+        [% l('Number of items: ') %] {{hold_data.copy_list.length}}
+      </div>
+      <div class="col-md-6 pull-right">
+        <input type="submit" class="btn btn-primary" value="[% l('OK') %]" ng-disabled="!hold_data.user"/>
+        <button class="btn btn-warning" ng-click="cancel($event)">[% l('Cancel') %]</button>
+      </div>
+    </div>
+  </div>
+</form>
diff --git a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
index 2608418..a709f04 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
@@ -7,13 +7,13 @@
  *
  */
 
-angular.module('egCatalogApp', ['ui.bootstrap','ngRoute','egCoreMod','egGridMod', 'egMarcMod'])
+angular.module('egCatalogApp', ['ui.bootstrap','ngRoute','egCoreMod','egGridMod', 'egMarcMod', 'egUserMod'])
 
 .config(function($routeProvider, $locationProvider, $compileProvider) {
     $locationProvider.html5Mode(true);
     $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|blob):/); // grid export
 
-    var resolver = {delay : ['egCore','egStartup', function(egCore,  egStartup) {
+    var resolver = {delay : ['egCore','egStartup','egUser', function(egCore, egStartup, egUser) {
         egCore.env.classLoaders.aous = function() {
             return egCore.org.settings([
                 'cat.marc_control_number_identifier'
@@ -227,9 +227,9 @@ function($scope , $routeParams , $location , $window , $q , egCore) {
 
 .controller('CatalogCtrl',
        ['$scope','$routeParams','$location','$window','$q','egCore','egHolds','egCirc',
-        'egGridDataProvider','egHoldGridActions','$timeout','holdingsSvc',
+        'egGridDataProvider','egHoldGridActions','$timeout','$modal','holdingsSvc','egUser',
 function($scope , $routeParams , $location , $window , $q , egCore , egHolds , egCirc, 
-         egGridDataProvider , egHoldGridActions , $timeout , holdingsSvc) {
+         egGridDataProvider , egHoldGridActions , $timeout , $modal , holdingsSvc , egUser) {
 
     // set record ID on page load if available...
     $scope.record_id = $routeParams.record_id;
@@ -316,6 +316,75 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
         }
     });
 
+    $scope.requestItems = function() {
+        var copy_list = gatherSelectedHoldingsIds();
+        if (copy_list.length == 0) return;
+
+        return $modal.open({
+            templateUrl: './cat/catalog/t_request_items',
+            animation: true,
+            controller:
+                   ['$scope','$modalInstance',
+            function($scope , $modalInstance) {
+                $scope.user = null;
+                $scope.first_user_fetch = true;
+
+                $scope.hold_data = {
+                    hold_type : 'C',
+                    copy_list : copy_list,
+                    pickup_lib: egCore.org.get(egCore.auth.user().ws_ou()),
+                    user      : egCore.auth.user().id()
+                };
+
+                egUser.get( $scope.hold_data.user ).then(function(u) {
+                    $scope.user = u;
+                    $scope.barcode = u.card().barcode();
+                    $scope.user_name = egUser.format_name(u);
+                    $scope.hold_data.user = u.id();
+                });
+
+                $scope.user_name = '';
+                $scope.barcode = '';
+                $scope.$watch('barcode', function (n) {
+                    if (!$scope.first_user_fetch) {
+                        egUser.getByBarcode(n).then(function(u) {
+                            $scope.user = u;
+                            $scope.user_name = egUser.format_name(u);
+                            $scope.hold_data.user = u.id();
+                        }, function() {
+                            $scope.user = null;
+                            $scope.user_name = '';
+                            delete $scope.hold_data.user;
+                        });
+                    }
+                    $scope.first_user_fetch = false;
+                });
+
+                $scope.ok = function(h) {
+                    var args = {
+                        patronid  : h.user,
+                        hold_type : h.hold_type,
+                        pickup_lib: h.pickup_lib.id(),
+                        depth     : 0
+                    };
+
+                    egCore.net.request(
+                        'open-ils.circ',
+                        'open-ils.circ.holds.test_and_create.batch.override',
+                        egCore.auth.token(), args, h.copy_list
+                    );
+
+                    $modalInstance.close();
+                }
+
+                $scope.cancel = function($event) {
+                    $modalInstance.dismiss();
+                    $event.preventDefault();
+                }
+            }]
+        });
+    }
+
     // refresh the list of holdings when the record_id is changed.
     $scope.holdings_record_id_changed = function(id) {
         if ($scope.record_id != id) $scope.record_id = id;
@@ -486,7 +555,6 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
                 'eg.cat.item_transfer_target',
                 $scope.holdingsGridControls.selectedItems()[0].call_number.id
             );
-            console.log('item_transfer_dest: '+$scope.holdingsGridControls.selectedItems()[0].call_number.id);
         }
     }
 
@@ -495,7 +563,6 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
             'eg.cat.volume_transfer_target',
             $scope.holdingsGridControls.selectedItems()[0].owner_id
         );
-        console.log('vol_transfer_dest: '+$scope.holdingsGridControls.selectedItems()[0].owner_id);
     }
 
     $scope.selectedHoldingsItemStatusDetail = function (){
diff --git a/Open-ILS/web/js/ui/default/staff/services/coresvc.js b/Open-ILS/web/js/ui/default/staff/services/coresvc.js
index de502cd..57b8361 100644
--- a/Open-ILS/web/js/ui/default/staff/services/coresvc.js
+++ b/Open-ILS/web/js/ui/default/staff/services/coresvc.js
@@ -10,7 +10,7 @@ angular.module('egCoreMod')
 .factory('egCore', 
        ['egIDL','egNet','egEnv','egOrg','egPCRUD','egEvent','egAuth',
         'egPerm','egHatch','egPrint','egStartup','egStrings','egDate',
-function(egIDL , egNet , egEnv , egOrg , egPCRUD , egEvent , egAuth , 
+function(egIDL , egNet , egEnv , egOrg , egPCRUD , egEvent , egAuth ,
          egPerm , egHatch , egPrint , egStartup , egStrings , egDate) {
 
     return {
diff --git a/Open-ILS/web/js/ui/default/staff/services/user.js b/Open-ILS/web/js/ui/default/staff/services/user.js
index 0ed5cac..f2a70c1 100644
--- a/Open-ILS/web/js/ui/default/staff/services/user.js
+++ b/Open-ILS/web/js/ui/default/staff/services/user.js
@@ -20,9 +20,20 @@ function($q,  $timeout,  egNet,  egAuth,  egOrg) {
         ]
     };
 
+    service.format_name = function(patron_obj) {
+        var patron_name = ( patron_obj.prefix() ? patron_obj.prefix() + ' ' : '') +
+            patron_obj.family_name() + ', ' +
+            patron_obj.first_given_name() + ' ' +
+            ( patron_obj.second_given_name() ? patron_obj.second_given_name() + ' ' : '' ) +
+            ( patron_obj.suffix() ? patron_obj.suffix() : '');
+        return patron_name;
+    };
+
     service.get = function(userId, args) {
         var deferred = $q.defer();
 
+        if (!userId) deferred.reject();
+
         var fields = service.defaultFleshFields;
         if (args) {
             if (args.useFields) { 
@@ -51,6 +62,19 @@ function($q,  $timeout,  egNet,  egAuth,  egOrg) {
         return deferred.promise;
     };
 
+    service.getByBarcode = function(barcode, args) {
+        return egNet.request(
+            'open-ils.pcrud',
+            'open-ils.pcrud.search.ac.atomic',
+            egAuth.token(), {barcode:barcode}
+        ).then( function(card) {
+            if (card && angular.isArray(card) && card[0] && card[0].classname == 'ac') {
+                return service.get(card[0].usr(), args)
+            }
+            return service.get(null);
+        }) 
+    };
+
     return service;
 }]);
 

commit cd9051bdb657ebbf4583f8208dce40809ba802f6
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Thu Sep 3 16:03:05 2015 +0000

    webstaff: make authorityControlSet in egTagTable actually be a singleton
    
    This resolves an issue where the control set can be incompletely
    initialized when jumping from creating a new bib to opening
    in in catalog view.
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js b/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
index b420749..58ec75b 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
@@ -518,7 +518,7 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
                 $scope.save_stack_depth = 0;
                 $scope.controlfields = [];
                 $scope.datafields = [];
-                $scope.controlSet = new egTagTable.authorityControlSet();
+                $scope.controlSet = egTagTable.getAuthorityControlSet();
 
                 egTagTable.loadTagTable({ marcRecordType : $scope.record_type });
 
diff --git a/Open-ILS/web/js/ui/default/staff/cat/services/tagtable.js b/Open-ILS/web/js/ui/default/staff/cat/services/tagtable.js
index f32b2de..1580ca5 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/services/tagtable.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/services/tagtable.js
@@ -17,7 +17,8 @@ function($q,   egCore,   egAuth) {
         authority_control_set : {
             _remote_loaded : false,
             _controlsets : [ ]
-        }
+        },
+        _active_control_set : undefined
     };
 
     service.initialized = function() {
@@ -500,5 +501,13 @@ function($q,   egCore,   egAuth) {
 
     }
 
+    service.getAuthorityControlSet = function() {
+        if (!service._active_control_set) {
+            service.authority_control_set._remote_loaded = false;
+            service._active_control_set = new service.authorityControlSet();
+        }
+        return service._active_control_set;
+    }
+
     return service;
 }]);

commit 803bfe7e91035e00818f8c609f8c59c633048e48
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Thu Sep 3 15:47:21 2015 +0000

    webstaff: implement Create New MARC Record
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/catalog/t_new_bib.tt2 b/Open-ILS/src/templates/staff/cat/catalog/t_new_bib.tt2
new file mode 100644
index 0000000..fdc9757
--- /dev/null
+++ b/Open-ILS/src/templates/staff/cat/catalog/t_new_bib.tt2
@@ -0,0 +1,13 @@
+<div class="form-inline row" ng-show="!have_template">
+    <div class="col-xs-6">
+        <div class="form-group">
+            <label for="select-marc-template" class="control-label">[% l('Select MARC template') %]</label>
+            <select id="select-marc-template" class="form-control" ng-model="template_name" ng-options="name as name for name in template_list"></select>
+        </div>
+        <button class="btn btn-primary" ng-click="loadTemplate()">[% l('Load') %]</button>
+        <button class="btn btn-default" ng-click="setDefaultTemplate()">[% l('Set Workstation Default') %]</button>
+    </div>
+</div>
+<div ng-show="have_template" class="row col-md-12">
+  <eg-marc-edit-record dirty-flag="stop_unload" record_id="new_bib_id" marc-xml="marc_template" record-type="bre" />
+</div>
diff --git a/Open-ILS/src/templates/staff/navbar.tt2 b/Open-ILS/src/templates/staff/navbar.tt2
index 52b4448..0c8748b 100644
--- a/Open-ILS/src/templates/staff/navbar.tt2
+++ b/Open-ILS/src/templates/staff/navbar.tt2
@@ -222,6 +222,12 @@
           </li>
           <li class="divider"></li>
           <li>
+            <a href="./cat/catalog/new_bib" target="_self">
+              <span class="glyphicon glyphicon-plus"></span>
+              [% l('Create New MARC Record') %]
+            </a>
+          </li>
+          <li>
             <a href="./cat/z3950/index" target="_self">
               <span class="glyphicon glyphicon-cloud-download"></span>
               [% l('Import Record from Z39.50') %]
diff --git a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
index 3dcea57..2608418 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
@@ -45,6 +45,12 @@ angular.module('egCatalogApp', ['ui.bootstrap','ngRoute','egCoreMod','egGridMod'
         resolve : resolver
     });
 
+    $routeProvider.when('/cat/catalog/new_bib', {
+        templateUrl: './cat/catalog/t_new_bib',
+        controller: 'NewBibCtrl',
+        resolve : resolver
+    });
+
     // create some catalog page-specific mappings
     $routeProvider.when('/cat/catalog/record/:record_id', {
         templateUrl: './cat/catalog/t_catalog',
@@ -163,6 +169,62 @@ function($scope , $routeParams , $location , $q , egCore ) {
 
 }])
 
+.controller('NewBibCtrl',
+       ['$scope','$routeParams','$location','$window','$q','egCore',
+        'egGridDataProvider','egHoldGridActions','$timeout','holdingsSvc',
+function($scope , $routeParams , $location , $window , $q , egCore) {
+
+    $scope.have_template = false;
+    $scope.marc_template = '';
+    $scope.stop_unload = false;
+    $scope.template_list = [];
+    $scope.template_name = '';
+    $scope.new_bib_id = 0;
+
+    egCore.net.request(
+        'open-ils.cat',
+        'open-ils.cat.marc_template.types.retrieve'
+    ).then(function(resp) {
+        angular.forEach(resp, function(name) {
+            $scope.template_list.push(name);
+        });
+        $scope.template_list.sort();
+    });
+    egCore.hatch.getItem('cat.default_bib_marc_template').then(function(template) {
+        $scope.template_name = template;
+    });
+
+    $scope.loadTemplate = function() {
+        if ($scope.template_name) {
+            egCore.net.request(
+                'open-ils.cat',
+                'open-ils.cat.biblio.marc_template.retrieve',
+                $scope.template_name
+            ).then(function(template) {
+                $scope.marc_template = template;
+                $scope.have_template = true;
+            });
+        }
+    }
+
+    $scope.setDefaultTemplate = function() {
+        var hatch_key = "cat.default_bib_marc_template";
+        if ($scope.template_name) {
+            egCore.hatch.setItem(hatch_key, $scope.template_name);
+        } else {
+            egCore.hatch.removeItem(hatch_key);
+        }
+    }
+
+    $scope.$watch('new_bib_id', function(newVal, oldVal) {
+        if (newVal) {
+            $location.path('/cat/catalog/record/' + $scope.new_bib_id);
+        }
+    });
+    
+
+}])
+
 .controller('CatalogCtrl',
        ['$scope','$routeParams','$location','$window','$q','egCore','egHolds','egCirc',
         'egGridDataProvider','egHoldGridActions','$timeout','holdingsSvc',
diff --git a/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js b/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
index f45376a..b420749 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
@@ -1026,6 +1026,13 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
                         }
                     }
                 );
+                $scope.$watch('marcXml',
+                    function(newVal, oldVal) {
+                        if (newVal && newVal !== oldVal) {
+                            loadRecord();
+                        }
+                    }
+                );
 
                 var unregister = $scope.$watch(function() {
                     return egTagTable.initialized();

commit 8071785b4e9588e1aae8aac9aaa1e071448216a3
Author: Mike Rylander <mrylander at gmail.com>
Date:   Thu Sep 3 09:54:33 2015 -0400

    webstaff: Edit items from copy buckets
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/bucket/copy/t_view.tt2 b/Open-ILS/src/templates/staff/cat/bucket/copy/t_view.tt2
index 07e5f23..a226dca 100644
--- a/Open-ILS/src/templates/staff/cat/bucket/copy/t_view.tt2
+++ b/Open-ILS/src/templates/staff/cat/bucket/copy/t_view.tt2
@@ -10,6 +10,8 @@
 
   [% INCLUDE 'staff/cat/bucket/copy/t_grid_menu.tt2' %]
 
+  <eg-grid-action label="[% l('Edit Selected Copies') %]" 
+    handler="spawnHoldingsEdit"></eg-grid-action>
   <eg-grid-action label="[% l('Remove Selected Copies') %]" 
     handler="detachCopies"></eg-grid-action>
   <eg-grid-action label="[% l('Delete Selected Copies from Catalog') %]" 
diff --git a/Open-ILS/web/js/ui/default/staff/cat/bucket/copy/app.js b/Open-ILS/web/js/ui/default/staff/cat/bucket/copy/app.js
index 1834de7..4291211 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/bucket/copy/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/bucket/copy/app.js
@@ -311,7 +311,6 @@ function($scope,  $location,  $q,  $timeout,  $modal,
         })
     }
 
-
     // opens the delete confirmation and deletes the current
     // bucket if the user confirms.
     $scope.openDeleteBucketDialog = function() {
@@ -402,9 +401,9 @@ function($scope,  $routeParams,  bucketSvc , egGridDataProvider,   egCore) {
 }])
 
 .controller('ViewCtrl',
-       ['$scope','$q','$routeParams','bucketSvc', 'egCore',
+       ['$scope','$q','$routeParams','$timeout','$window','bucketSvc','egCore',
         'egConfirmDialog',
-function($scope,  $q , $routeParams,  bucketSvc, egCore,
+function($scope,  $q , $routeParams , $timeout , $window , bucketSvc , egCore,
          egConfirmDialog) {
 
     $scope.setTab('view');
@@ -449,6 +448,35 @@ function($scope,  $q , $routeParams,  bucketSvc, egCore,
         return $q.all(promises).then(drawBucket);
     }
 
+    $scope.spawnHoldingsEdit = function (copies) {
+        var rec_hash = {};
+        angular.forEach($scope.gridControls.selectedItems(), function (i) {
+            var rec = i['call_number.record.id'];
+            if (!rec_hash[rec]) rec_hash[rec] = [];
+            rec_hash[rec].push(i.id);
+        })
+
+        angular.forEach(rec_hash, function(cp_list,r) {
+            egCore.net.request(
+                'open-ils.actor',
+                'open-ils.actor.anon_cache.set_value',
+                null, 'edit-these-copies', {
+                    record_id: r,
+                    copies: cp_list,
+                    hide_vols : true,
+                    hide_copies : false
+                }
+            ).then(function(key) {
+                if (key) {
+                    var url = egCore.env.basePath + 'cat/volcopy/' + key;
+                    $timeout(function() { $window.open(url, '_blank') });
+                } else {
+                    alert('Could not create anonymous cache key!');
+                }
+            });
+        });
+    }
+
     $scope.deleteCopiesFromCatalog = function(copies) {
         egConfirmDialog.open(
             egCore.strings.CONFIRM_DELETE_COPY_BUCKET_ITEMS_FROM_CATALOG,

commit bacc90afece1503dd701df2bb6b1f7270c26c7a3
Author: Mike Rylander <mrylander at gmail.com>
Date:   Thu Sep 3 09:11:30 2015 -0400

    webstaff: Support adding copies and volumes
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2 b/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2
index 27d434a..5ef56d1 100644
--- a/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2
+++ b/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2
@@ -51,6 +51,11 @@
     <eg-grid-action handler="markVolAsItemTarget" group="[% l('Mark') %]" disabled="vols_not_shown"
       label="[% l('Volume as Item Transfer Destination') %]"></eg-grid-action>
 
+    <eg-grid-action handler="selectedHoldingsCopyAdd" group="[% l('Add') %]" disabled="vols_not_shown"
+      label="[% l('Copies') %]"></eg-grid-action>
+    <eg-grid-action handler="selectedHoldingsVolCopyAdd" group="[% l('Add') %]"
+      label="[% l('Volumes and Copies') %]"></eg-grid-action>
+
     <eg-grid-action handler="selectedHoldingsVolEdit" group="[% l('Edit') %]"
       label="[% l('Volumes') %]"></eg-grid-action>
     <eg-grid-action handler="selectedHoldingsCopyEdit" group="[% l('Edit') %]"
diff --git a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
index c108774..3dcea57 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
@@ -354,6 +354,42 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
         return cn_id_list;
     }
 
+    spawnHoldingsAdd = function (hide_vols,hide_copies){
+        var raw = [];
+        if (hide_vols) { // just a copy on existing volumes
+            angular.forEach(gatherSelectedVolumeIds(), function (v) {
+                raw.push( {callnumber : v} );
+            });
+        } else {
+            angular.forEach(
+                $scope.holdingsGridControls.selectedItems(),
+                function (item) {
+                    raw.push({owner : item.owner_id});
+                }
+            );
+        }
+
+        egCore.net.request(
+            'open-ils.actor',
+            'open-ils.actor.anon_cache.set_value',
+            null, 'edit-these-copies', {
+                record_id: $scope.record_id,
+                raw: raw,
+                hide_vols : hide_vols,
+                hide_copies : hide_copies
+            }
+        ).then(function(key) {
+            if (key) {
+                var url = egCore.env.basePath + 'cat/volcopy/' + key;
+                $timeout(function() { $window.open(url, '_blank') });
+            } else {
+                alert('Could not create anonymous cache key!');
+            }
+        });
+    }
+    $scope.selectedHoldingsVolCopyAdd = function () { spawnHoldingsAdd(false,false) }
+    $scope.selectedHoldingsCopyAdd = function () { spawnHoldingsAdd(true,false) }
+
     spawnHoldingsEdit = function (hide_vols,hide_copies){
         egCore.net.request(
             'open-ils.actor',
@@ -864,6 +900,7 @@ function(egCore , $q) {
                             current_blob.call_number = cp.call_number;
                             current_blob.owner_list = cp.owner_list;
                             current_blob.owner_label = cp.owner_label;
+                            current_blob.owner_id = cp.owner_id;
                         } else {
                             var current_key = cp.owner_list.join('') + cp.call_number.label;
                             if (prev_key == current_key) { // collapse into current_blob
@@ -880,6 +917,7 @@ function(egCore , $q) {
                                 current_blob.id_list = cp.id_list;
                                 current_blob.raw = cp.raw;
                                 current_blob.owner_label = cp.owner_label;
+                                current_blob.owner_id = cp.owner_id;
                                 current_blob.call_number = cp.call_number;
                                 current_blob.owner_list = cp.owner_list;
                             }
@@ -906,6 +944,7 @@ function(egCore , $q) {
                                 current_blob.copy_count = cp.copy_count;
                                 current_blob.owner_list = cp.owner_list;
                                 current_blob.owner_label = cp.owner_label;
+                                current_blob.owner_id = cp.owner_id;
                             } else {
                                 var current_key = cp.owner_list.join('');
                                 if (prev_key == current_key) { // collapse into current_blob
@@ -923,6 +962,7 @@ function(egCore , $q) {
                                     current_blob.id_list = cp.id_list;
                                     current_blob.raw = cp.raw;
                                     current_blob.owner_label = cp.owner_label;
+                                    current_blob.owner_id = cp.owner_id;
                                     current_blob.cn_count = 1;
                                     current_blob.copy_count = cp.copy_count;
                                     current_blob.owner_list = cp.owner_list;
diff --git a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
index 638a77f..8cd4b78 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
@@ -32,6 +32,8 @@ angular.module('egVolCopy',
 function(egCore , $q) {
 
     var service = {
+        new_cp_id : 0,
+        new_cn_id : 0,
         tree : {}, // holds lib->cn->copy hash stack
         copies : [] // raw copy list
     };
@@ -282,7 +284,6 @@ function(egCore , $q) {
         scope: {allcopies: "=", copies: "=" },
         controller : ['$scope','itemSvc','egCore',
             function ( $scope , itemSvc , egCore ) {
-                $scope.new_cp_id = 0;
                 $scope.callNumber =  $scope.copies[0].call_number();
 
                 $scope.idTracker = function (x) { if (x) return x.id() };
@@ -391,7 +392,7 @@ function(egCore , $q) {
                 $scope.changeCPCount = function () {
                     while ($scope.copy_count > $scope.copies.length) {
                         var cp = new egCore.idl.acp();
-                        cp.id( --$scope.new_cp_id );
+                        cp.id( --itemSvc.new_cp_id );
                         cp.isnew( true );
                         cp.circ_lib( $scope.lib );
                         cp.call_number( $scope.callNumber );
@@ -437,7 +438,6 @@ function(egCore , $q) {
         scope: { allcopies: "=", struct: "=", lib: "@", record: "@" },
         controller : ['$scope','itemSvc','egCore',
             function ( $scope , itemSvc , egCore ) {
-                $scope.new_cn_id = 0;
                 $scope.first_cn = Object.keys($scope.struct)[0];
                 $scope.full_cn = $scope.struct[$scope.first_cn][0].call_number();
 
@@ -458,13 +458,13 @@ function(egCore , $q) {
                     if (n > o) { // adding
                         for (var i = o; o < n; o++) {
                             var cn = new egCore.idl.acn();
-                            cn.id( --$scope.new_cn_id );
+                            cn.id( --itemSvc.new_cn_id );
                             cn.isnew( true );
                             cn.owning_lib( $scope.owning_lib.id() );
                             cn.record( $scope.full_cn.record() );
 
                             var cp = new egCore.idl.acp();
-                            cp.id( --$scope.new_cp_id );
+                            cp.id( --itemSvc.new_cp_id );
                             cp.isnew( true );
                             cp.circ_lib( $scope.owning_lib.id() );
                             cp.call_number( cn );
@@ -810,17 +810,51 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
 
                 if (data.raw && data.raw.length) {
 
-                    /* data.raw must be an array of copies with (at least)
-                     * the call number fleshed on each.  For new copies
-                     * create from whole cloth, the id for each should
-                     * probably be negative and isnew() should return true.
-                     * Each /distinct/ call number must have a distinct id
-                     * as well, probably negative also if they're new. Clear?
+                    /* data.raw data structure looks like this:
+                     * [{
+                     *      callnumber : $cn_id, // optional, to add a copy to a cn
+                     *      owner      : $org, // optional, defaults to ws_ou
+                     *      label      : $cn_label, // optional, to supply a label on a new cn
+                     *      barcode    : $cp_barcode // optional, to supply a barcode on a new cp
+                     * },...]
+                     * 
+                     * All can be left out and a completely empty vol/copy combo will be vivicated.
                      */
 
                     angular.forEach(
                         data.raw,
-                        function (cp) { itemSvc.addCopy(cp) }
+                        function (proto) {
+                            if (proto.callnumber) {
+                                return egCore.pcrud.retrieve('acn', proto.callnumber)
+                                .then(function(cn) {
+                                    var cp = new egCore.idl.acp();
+                                    cp.call_number( cn );
+                                    cp.id( --itemSvc.new_cp_id );
+                                    cp.isnew( true );
+                                    cp.circ_lib( $scope.record_id );
+                                    if (proto.barcode) cp.barcode( proto.barcode );
+
+                                    itemSvc.addCopy(cp)
+                                });
+                            } else {
+                                var cn = new egCore.idl.acn();
+                                cn.id( --itemSvc.new_cn_id );
+                                cn.isnew( true );
+                                cn.owning_lib( proto.owner || egCore.auth.user().ws_ou() );
+                                cn.record( $scope.record_id );
+                                if (proto.label) cn.label( proto.label );
+
+                                var cp = new egCore.idl.acp();
+                                cp.call_number( cn );
+                                cp.id( --itemSvc.new_cp_id );
+                                cp.isnew( true );
+                                cp.circ_lib( $scope.record_id );
+                                if (proto.barcode) cp.barcode( proto.barcode );
+
+                                itemSvc.addCopy(cp)
+                            }
+    
+                        }
                     );
 
                     return itemSvc.copies;

commit f9f04b4eb71cedf9e866b99a8ac5a9dfa207f218
Author: Mike Rylander <mrylander at gmail.com>
Date:   Thu Sep 3 08:21:20 2015 -0400

    webstaff: Transfer volume and item actions
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2 b/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2
index 8cf4396..27d434a 100644
--- a/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2
+++ b/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2
@@ -41,16 +41,27 @@
       label="[% l('Triggered Events') %]"></eg-grid-action>
     <eg-grid-action handler="selectedHoldingsItemStatusHolds" group="[% l('Show') %]"
       label="[% l('Item Holds') %]"></eg-grid-action>
+
     <eg-grid-action handler="selectedHoldingsDamaged" group="[% l('Mark') %]"
       label="[% l('Item as Damaged') %]"></eg-grid-action>
     <eg-grid-action handler="selectedHoldingsMissing" group="[% l('Mark') %]"
       label="[% l('Item as Missing') %]"></eg-grid-action>
+    <eg-grid-action handler="markLibAsVolTarget" group="[% l('Mark') %]"
+      label="[% l('Library as Volume Transfer Destination') %]"></eg-grid-action>
+    <eg-grid-action handler="markVolAsItemTarget" group="[% l('Mark') %]" disabled="vols_not_shown"
+      label="[% l('Volume as Item Transfer Destination') %]"></eg-grid-action>
+
     <eg-grid-action handler="selectedHoldingsVolEdit" group="[% l('Edit') %]"
-      label="[% l('Selected Volumes') %]"></eg-grid-action>
+      label="[% l('Volumes') %]"></eg-grid-action>
     <eg-grid-action handler="selectedHoldingsCopyEdit" group="[% l('Edit') %]"
-      label="[% l('Selected Copies') %]"></eg-grid-action>
+      label="[% l('Copies') %]"></eg-grid-action>
     <eg-grid-action handler="selectedHoldingsVolCopyEdit" group="[% l('Edit') %]"
-      label="[% l('Selected Volumes and Copies') %]"></eg-grid-action>
+      label="[% l('Volumes and Copies') %]"></eg-grid-action>
+
+    <eg-grid-action handler="transferVolumes" group="[% l('Transfer') %]"
+      label="[% l('Volumes to Previously Marked Library') %]"></eg-grid-action>
+    <eg-grid-action handler="transferItems" group="[% l('Transfer') %]"
+      label="[% l('Items to Previously Marked Volume') %]"></eg-grid-action>
 
     <eg-grid-field label="[% l('Owning Library') %]"  path="owner_label" flex="4" align="right" visible></eg-grid-field>
     <eg-grid-field label="[% l('Call Number') %]"     path="call_number.label" visible></eg-grid-field>
diff --git a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
index 61ad57f..c108774 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
@@ -316,6 +316,10 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
         })
     });
 
+    $scope.vols_not_shown = function () {
+        return !$scope.holdings_show_vols;
+    }
+
     $scope.holdings_checkbox_handler = function (item) {
         $scope.holdings_cb_changed(item.checkbox,item.checked);
     }
@@ -329,6 +333,27 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
         return cp_id_list;
     }
 
+    function gatherSelectedRawCopies () {
+        var cp_list = [];
+        angular.forEach(
+            $scope.holdingsGridControls.selectedItems(),
+            function (item) { cp_list = cp_list.concat(item.raw) }
+        );
+        return cp_list;
+    }
+
+    function gatherSelectedVolumeIds () {
+        var cn_id_list = [];
+        angular.forEach(
+            $scope.holdingsGridControls.selectedItems(),
+            function (item) {
+                if (cn_id_list.indexOf(item.call_number.id) == -1)
+                    cn_id_list.push(item.call_number.id)
+            }
+        );
+        return cn_id_list;
+    }
+
     spawnHoldingsEdit = function (hide_vols,hide_copies){
         egCore.net.request(
             'open-ils.actor',
@@ -357,6 +382,24 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
         $timeout(function() { $window.open(url, '_blank') });
     }
 
+    $scope.markVolAsItemTarget = function() {
+        if ($scope.holdingsGridControls.selectedItems()[0].call_number.id) { // cn.id missing when vols are collapsed
+            egCore.hatch.setLocalItem(
+                'eg.cat.item_transfer_target',
+                $scope.holdingsGridControls.selectedItems()[0].call_number.id
+            );
+            console.log('item_transfer_dest: '+$scope.holdingsGridControls.selectedItems()[0].call_number.id);
+        }
+    }
+
+    $scope.markLibAsVolTarget = function() {
+        egCore.hatch.setLocalItem(
+            'eg.cat.volume_transfer_target',
+            $scope.holdingsGridControls.selectedItems()[0].owner_id
+        );
+        console.log('vol_transfer_dest: '+$scope.holdingsGridControls.selectedItems()[0].owner_id);
+    }
+
     $scope.selectedHoldingsItemStatusDetail = function (){
         angular.forEach(
             gatherSelectedHoldingsIds(),
@@ -368,6 +411,67 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
         );
     }
 
+    $scope.transferVolumes = function (){
+        var xfer_target = egCore.hatch.getLocalItem('eg.cat.volume_transfer_target');
+
+        if (xfer_target) {
+            egCore.net.request(
+                'open-ils.cat',
+                'open-ils.open-ils.cat.asset.volume.batch.transfer.override',
+                egCore.auth.token(), {
+                    docid   : $scope.record_id,
+                    lib     : xfer_target,
+                    volumes : gatherSelectedVolumeIds()
+                }
+            ).then(function(success) {
+                if (success) {
+                    holdingsSvc.fetch({
+                        rid : $scope.record_id,
+                        org : $scope.holdings_ou,
+                        copy: $scope.holdings_show_copies,
+                        vol : $scope.holdings_show_vols,
+                        empty: $scope.holdings_show_empty
+                    }).then(function() {
+                        $scope.holdingsGridDataProvider.refresh();
+                    });
+                } else {
+                    alert('Could not transfer volumes!');
+                }
+            });
+        }
+        
+    }
+
+    $scope.transferItems = function (){
+        var xfer_target = egCore.hatch.getLocalItem('eg.cat.item_transfer_target');
+        if (xfer_target) {
+            var copy_list = gatherSelectedRawCopies();
+
+            angular.forEach(copy_list, function (cp) {
+                cp.call_number(xfer_target);
+            });
+
+            egCore.pcrud.update(
+                copy_list
+            ).then(function(success) {
+                if (success) {
+                    holdingsSvc.fetch({
+                        rid : $scope.record_id,
+                        org : $scope.holdings_ou,
+                        copy: $scope.holdings_show_copies,
+                        vol : $scope.holdings_show_vols,
+                        empty: $scope.holdings_show_empty
+                    }).then(function() {
+                        $scope.holdingsGridDataProvider.refresh();
+                    });
+                } else {
+                    alert('Could not transfer items!');
+                }
+            });
+        }
+        
+    }
+
     $scope.selectedHoldingsItemStatusTgrEvt = function (){
         angular.forEach(
             gatherSelectedHoldingsIds(),
@@ -756,6 +860,7 @@ function(egCore , $q) {
                             if (cp.barcode) current_blob.copy_count = 1;
                             current_blob.index = index++;
                             current_blob.id_list = cp.id_list;
+                            current_blob.raw = cp.raw;
                             current_blob.call_number = cp.call_number;
                             current_blob.owner_list = cp.owner_list;
                             current_blob.owner_label = cp.owner_label;
@@ -764,6 +869,7 @@ function(egCore , $q) {
                             if (prev_key == current_key) { // collapse into current_blob
                                 current_blob.copy_count++;
                                 current_blob.id_list = current_blob.id_list.concat(cp.id_list);
+                                current_blob.raw = current_blob.raw.concat(cp.raw);
                             } else {
                                 current_blob.barcode = current_blob.copy_count;
                                 cp_list.push(current_blob);
@@ -772,6 +878,7 @@ function(egCore , $q) {
                                 if (cp.barcode) current_blob.copy_count = 1;
                                 current_blob.index = index++;
                                 current_blob.id_list = cp.id_list;
+                                current_blob.raw = cp.raw;
                                 current_blob.owner_label = cp.owner_label;
                                 current_blob.call_number = cp.call_number;
                                 current_blob.owner_list = cp.owner_list;
@@ -794,6 +901,7 @@ function(egCore , $q) {
                                 prev_key = cp.owner_list.join('');
                                 current_blob.index = index++;
                                 current_blob.id_list = cp.id_list;
+                                current_blob.raw = cp.raw;
                                 current_blob.cn_count = 1;
                                 current_blob.copy_count = cp.copy_count;
                                 current_blob.owner_list = cp.owner_list;
@@ -804,6 +912,7 @@ function(egCore , $q) {
                                     current_blob.cn_count++;
                                     current_blob.copy_count += cp.copy_count;
                                     current_blob.id_list = current_blob.id_list.concat(cp.id_list);
+                                    current_blob.raw = current_blob.raw.concat(cp.raw);
                                 } else {
                                     current_blob.barcode = current_blob.copy_count;
                                     current_blob.call_number = { label : current_blob.cn_count };
@@ -812,6 +921,7 @@ function(egCore , $q) {
                                     current_blob = {};
                                     current_blob.index = index++;
                                     current_blob.id_list = cp.id_list;
+                                    current_blob.raw = cp.raw;
                                     current_blob.owner_label = cp.owner_label;
                                     current_blob.cn_count = 1;
                                     current_blob.copy_count = cp.copy_count;
@@ -844,29 +954,33 @@ function(egCore , $q) {
                     cp.call_number(cn);
                 });
 
-                var flat = egCore.idl.toHash(copies);
-                if (flat[0]) {
-                    var owner = egCore.org.get(flat[0].call_number.owning_lib);
+                var owner_id = cn.owning_lib();
+                var owner = egCore.org.get(owner_id);
 
-                    var owner_name_list = [];
-                    while (owner.parent_ou()) { // we're going to skip the top of the tree...
-                        owner_name_list.unshift(owner.name());
-                        owner = egCore.org.get(owner.parent_ou());
-                    }
+                var owner_name_list = [];
+                while (owner.parent_ou()) { // we're going to skip the top of the tree...
+                    owner_name_list.unshift(owner.name());
+                    owner = egCore.org.get(owner.parent_ou());
+                }
 
-                    angular.forEach(flat, function (cp) {
-                        cp.owner_list = owner_name_list;
-                        cp.id_list = [cp.id];
+                if (copies[0]) {
+                    var flat = [];
+                    angular.forEach(copies, function (cp) {
+                        var flat_cp = egCore.idl.toHash(cp);
+                        flat_cp.owner_id = owner_id;
+                        flat_cp.owner_list = owner_name_list;
+                        flat_cp.id_list = [flat_cp.id];
+                        flat_cp.raw = [cp];
+                        flat.push(flat_cp);
                     });
 
                     service.copies = service.copies.concat(flat);
-
-                    if (empty && flat.length == 0) {
-                        service.copies.push({
-                            owner_list : owner_name_list,
-                            call_number: egCore.idl.toHash(cn)
-                        });
-                    }
+                } else if (empty) {
+                    service.copies.push({
+                        owner_id   : owner_id,
+                        owner_list : owner_name_list,
+                        call_number: egCore.idl.toHash(cn)
+                    });
                 }
 
                 return cn;
diff --git a/Open-ILS/web/js/ui/default/staff/services/grid.js b/Open-ILS/web/js/ui/default/staff/services/grid.js
index 02e462c..66a2bf3 100644
--- a/Open-ILS/web/js/ui/default/staff/services/grid.js
+++ b/Open-ILS/web/js/ui/default/staff/services/grid.js
@@ -556,18 +556,22 @@ angular.module('egGridMod',
 
             // fires the hide handler function for a context action
             $scope.actionHide = function(action) {
-                if (!action.hide) {
+                if (typeof action.hide == 'undefined') {
                     return false;
                 }
-                return action.hide(action);
+                if (angular.isFunction(action.hide))
+                    return action.hide(action);
+                return action.hide;
             }
 
             // fires the disable handler function for a context action
             $scope.actionDisable = function(action) {
-                if (!action.disabled) {
+                if (typeof action.disabled == 'undefined') {
                     return false;
                 }
-                return action.disabled(action);
+                if (angular.isFunction(action.disabled))
+                    return action.disabled(action);
+                return action.disabled;
             }
 
             // fires the action handler function for a context action

commit 3530113c1acfde66e45a3f67ba65ca43de52c2a4
Author: Mike Rylander <mrylander at gmail.com>
Date:   Wed Sep 2 16:45:42 2015 -0400

    webstaff: Avoid adding deleted notes that were created in the same session
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Cat/AssetCommon.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Cat/AssetCommon.pm
index 8d642b3..fe0913c 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Cat/AssetCommon.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Cat/AssetCommon.pm
@@ -236,6 +236,8 @@ sub update_copy_notes {
         next unless $incoming_note;
 
         if ($incoming_note->isnew) {
+            next if ($incoming_note->isdeleted); # if it was added and deleted in the same session
+
             my $new_note = Fieldmapper::asset::copy_note->new();
             $new_note->owning_copy( $copy->id );
             $new_note->pub( $incoming_note->pub );
@@ -244,6 +246,7 @@ sub update_copy_notes {
             $new_note->creator( $incoming_note->creator || $editor->requestor->id );
             $incoming_note = $editor->create_asset_copy_note($new_note)
                 or return $editor->event;
+
         } elsif ($incoming_note->ischanged) {
             $incoming_note = $editor->update_asset_copy_note($incoming_note)
         } elsif ($incoming_note->isdeleted) {

commit e2f6cca04f506e0500cbddb80806c87f12b91edb
Author: Mike Rylander <mrylander at gmail.com>
Date:   Wed Sep 2 16:43:16 2015 -0400

    webstaff: Copy note bug fixing
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Cat/AssetCommon.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Cat/AssetCommon.pm
index 7b0ea44..8d642b3 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Cat/AssetCommon.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Cat/AssetCommon.pm
@@ -228,7 +228,6 @@ sub update_copy_notes {
     my($class, $editor, $copy) = @_;
 
     return undef if $copy->isdeleted;
-    return undef unless $copy->ischanged or $copy->isnew;
 
     my $evt;
     my $incoming_notes = $copy->notes;
@@ -245,9 +244,9 @@ sub update_copy_notes {
             $new_note->creator( $incoming_note->creator || $editor->requestor->id );
             $incoming_note = $editor->create_asset_copy_note($new_note)
                 or return $editor->event;
-        } elif ($incoming_note->ischanged) {
+        } elsif ($incoming_note->ischanged) {
             $incoming_note = $editor->update_asset_copy_note($incoming_note)
-        } elif ($incoming_note->isdeleted) {
+        } elsif ($incoming_note->isdeleted) {
             $incoming_note = $editor->delete_asset_copy_note($incoming_note->id)
         }
     
@@ -360,11 +359,17 @@ sub update_fleshed_copies {
         $copy->location( $copy->location->id ) if ref($copy->location);
         $copy->circ_lib( $copy->circ_lib->id ) if ref($copy->circ_lib);
         
+        my $parts = $copy->parts;
+        $copy->clear_parts;
+
         my $sc_entries = $copy->stat_cat_entries;
         $copy->clear_stat_cat_entries;
 
-        my $parts = $copy->parts;
-        $copy->clear_parts;
+        my $sc_entries = $copy->stat_cat_entries;
+        $copy->clear_stat_cat_entries;
+
+        my $notes = $copy->notes;
+        $copy->clear_notes;
 
         if( $copy->isdeleted ) {
             $evt = $class->delete_copy($editor, $override, $vol, $copy, $retarget_holds, $force_delete_empty_bib);
@@ -382,9 +387,12 @@ sub update_fleshed_copies {
 
         $copy->stat_cat_entries( $sc_entries );
         $evt = $class->update_copy_stat_entries($editor, $copy, $delete_stats);
+
         $copy->parts( $parts );
         # probably okay to use $delete_stats here for simplicity
         $evt = $class->update_copy_parts($editor, $copy, $delete_stats, $create_parts);
+
+        $copy->notes( $notes );
         $evt = $class->update_copy_notes($editor, $copy);
         return $evt if $evt;
     }
diff --git a/Open-ILS/src/templates/staff/cat/volcopy/t_copy_notes.tt2 b/Open-ILS/src/templates/staff/cat/volcopy/t_copy_notes.tt2
index 7bd26a7..8b875f0 100644
--- a/Open-ILS/src/templates/staff/cat/volcopy/t_copy_notes.tt2
+++ b/Open-ILS/src/templates/staff/cat/volcopy/t_copy_notes.tt2
@@ -52,7 +52,7 @@
         </div>
       </div>
 
-      <div class="row" ng-repeat="n in note_list" ng-init="pub = n.pub(); title = n.title(); value = n.value(); deleted = n.isdeleted()">
+      <div class="row" ng-repeat="n in note_list" ng-init="pub = n.pub() == 't'; title = n.title(); value = n.value(); deleted = n.isdeleted()">
         <div class="col-md-12">
           <div class="row">
             <div class="col-md-6">

commit 3b89ef139e2d98783493b9a7e134fd34a3ea5fed
Author: Mike Rylander <mrylander at gmail.com>
Date:   Wed Sep 2 16:12:44 2015 -0400

    webstaff: Add copy note creation/editing
    
    TODO: move copy note dialog to a service so others can use it
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2 b/Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2
index d427704..9a0b48e 100644
--- a/Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2
+++ b/Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2
@@ -360,6 +360,15 @@
                         <option value="3">[% l('High') %]</option>
                     </select>
                 </div>
+                <div class="col-md-6">
+                    <button
+                      class="btn btn-default"
+                      ng-disabled="!defaults.copy_notes"
+                      ng-click="copy_notes_dialog(workingGridControls.selectedItems())"
+                      type="button">
+                        [% l('Copy Notes') %]
+                    </button>
+                </div>
             </div>
         </div>
 
diff --git a/Open-ILS/src/templates/staff/cat/volcopy/t_copy_notes.tt2 b/Open-ILS/src/templates/staff/cat/volcopy/t_copy_notes.tt2
new file mode 100644
index 0000000..7bd26a7
--- /dev/null
+++ b/Open-ILS/src/templates/staff/cat/volcopy/t_copy_notes.tt2
@@ -0,0 +1,91 @@
+<form ng-submit="ok(note)" role="form">
+    <div class="modal-header">
+      <button type="button" class="close" ng-click="cancel()" 
+        aria-hidden="true">×</button>
+      <h4 class="modal-title">[% l('New Copy Note') %]</h4>
+    </div>
+    <div class="modal-body">
+      <div class="row">
+        <div class="col-md-6">
+          <input class="form-control" type="text"
+            ng-model="note.title" placeholder="[% l('Title...') %]"/>
+        </div>
+        <div class="col-md-3">
+          <label>
+            <input type="checkbox" ng-model="note.pub"/>
+            [% l('Public Note') %]
+          </label>
+        </div>
+      </div>
+      <div class="row pad-vert">
+        <div class="col-md-12">
+          <textarea class="form-control" 
+            ng-model="note.value" placeholder="[% l('Note...') %]">
+          </textarea>
+        </div>
+      </div>
+    </div>
+    <div class="modal-footer">
+      <div class="row">
+        <div class="col-md-2">
+          <input type="text" class="form-control" ng-hide="!require_initials" 
+            ng-model="initials" placeholder="[% l('Initials') %]" ng-required="require_initials"/>
+        </div>
+        <div class="col-md-10 pull-right">
+          <input type="submit" class="btn btn-primary" value="[% l('OK') %]"/>
+          <button class="btn btn-warning" ng-click="cancel($event)">[% l('Cancel') %]</button>
+        </div>
+      </div>
+
+      <div class="row pad-vert" ng-if="note_list.length > 0"> 
+        <div class="col-md-12">
+          <div class="row">
+            <div class="col-md-12">
+              <hr/>
+            </div>
+          </div>
+          <div class="row">
+            <div class="col-md-12">
+              <h4 class="pull-left">[% l('Existing Copy Notes') %]</h4>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <div class="row" ng-repeat="n in note_list" ng-init="pub = n.pub(); title = n.title(); value = n.value(); deleted = n.isdeleted()">
+        <div class="col-md-12">
+          <div class="row">
+            <div class="col-md-6">
+              <input class="form-control" type="text" ng-change="n.title(title) && n.ischanged(1)"
+                ng-model="title" placeholder="[% l('Title...') %]" ng-disabled="deleted"/>
+            </div>
+            <div class="col-md-3">
+              <label>
+                <input type="checkbox" ng-model="pub" ng-change="n.pub(pub) && n.ischanged(1)" ng-disabled="deleted"/>
+                [% l('Public Note') %]
+              </label>
+            </div>
+            <div class="col-md-3">
+              <label>
+                <input type="checkbox" ng-model="deleted" ng-change="n.isdeleted(deleted)"/>
+                [% l('Deleted?') %]
+              </label>
+            </div>
+          </div>
+          <div class="row pad-vert">
+            <div class="col-md-12">
+              <textarea class="form-control" ng-change="n.value(value) && n.ischanged(1)"
+                ng-model="value" placeholder="[% l('Note...') %]" ng-disabled="deleted">
+              </textarea>
+            </div>
+          </div>
+          <div class="row">
+            <div class="col-md-12">
+              <hr/>
+            </div>
+          </div>
+        </div>
+      </div>
+
+    </div>
+</form>
diff --git a/Open-ILS/src/templates/staff/cat/volcopy/t_defaults.tt2 b/Open-ILS/src/templates/staff/cat/volcopy/t_defaults.tt2
index f589139..6c356c2 100644
--- a/Open-ILS/src/templates/staff/cat/volcopy/t_defaults.tt2
+++ b/Open-ILS/src/templates/staff/cat/volcopy/t_defaults.tt2
@@ -229,6 +229,10 @@
                     </label>
                 </div>
                 <div class="col-xs-6">
+                    <label>
+                        <input type="checkbox" ng-change="saveDefaults()" ng-model="defaults.copy_notes"/>
+                        [% l('Add/Edit Copy Notes') %]
+                    </label>
                 </div>
             </div>
 
@@ -240,7 +244,10 @@
                     </label>
                 </div>
                 <div class="col-xs-6">
-                    <h6>[% l('Statistical Categories') %]</h6>
+                    <label>
+                        <input type="checkbox" ng-change="saveDefaults()" ng-model="defaults.copy_notes_pub"/>
+                        [% l('Copy Notes are Public') %]
+                    </label>
                 </div>
             </div>
 
@@ -252,9 +259,7 @@
                     </label>
                 </div>
                 <div class="col-xs-6">
-                    <label>
-                        <eg-org-selector selected="defaults.statcat_filter" noDefault label="[% l('Default Filter Library') %]" disableTest="cant_have_vols"></eg-org-selector>
-                    </label>
+                    <h6>[% l('Statistical Categories') %]</h6>
                 </div>
             </div>
 
@@ -267,8 +272,10 @@
                 </div>
                 <div class="col-xs-6">
                     <label>
-                        <input type="checkbox" ng-change="saveDefaults()" ng-model="defaults.statcats"/>
-                        [% l('Edit Statistical Data') %]
+                        <eg-org-selector
+                            selected="defaults.statcat_filter"
+                            noDefault label="[% l('Default Filter Library') %]"
+                            disableTest="cant_have_vols"></eg-org-selector>
                     </label>
                 </div>
             </div>
@@ -281,6 +288,10 @@
                     </label>
                 </div>
                 <div class="col-xs-6">
+                    <label>
+                        <input type="checkbox" ng-change="saveDefaults()" ng-model="defaults.statcats"/>
+                        [% l('Edit Statistical Data') %]
+                    </label>
                 </div>
             </div>
 
diff --git a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
index 87f734a..638a77f 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
@@ -153,7 +153,7 @@ function(egCore , $q) {
     service.flesh = {   
         flesh : 3, 
         flesh_fields : {
-            acp : ['call_number','parts','stat_cat_entries'],
+            acp : ['call_number','parts','stat_cat_entries', 'notes'],
             acn : ['label_class','prefix','suffix']
         }
     }
@@ -499,11 +499,12 @@ function(egCore , $q) {
  * Edit controller!
  */
 .controller('EditCtrl', 
-       ['$scope','$q','$window','$routeParams','$location','$timeout','egCore','egNet','egGridDataProvider','itemSvc',
-function($scope , $q , $window , $routeParams , $location , $timeout , egCore , egNet , egGridDataProvider , itemSvc) {
+       ['$scope','$q','$window','$routeParams','$location','$timeout','egCore','egNet','egGridDataProvider','itemSvc','$modal',
+function($scope , $q , $window , $routeParams , $location , $timeout , egCore , egNet , egGridDataProvider , itemSvc , $modal) {
 
     $scope.defaults = { // If defaults are not set at all, allow everything
         statcats : true,
+        copy_notes : true,
         attributes : {
             status : true,
             loan_duration : true,
@@ -1048,7 +1049,6 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
                 'open-ils.cat.asset.volume.fleshed.batch.update.override',
                 egCore.auth.token(), cnList, 1, { auto_merge_vols : 1, create_parts : 1 }
             ).then(function(update_count) {
-                alert(update_count + ' call numbers updated');
                 if (and_exit) {
                     $scope.dirty = false;
                     $timeout(function(){$window.close()});
@@ -1062,6 +1062,60 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
 
     }
 
+    $scope.copy_notes_dialog = function(copy_list) {
+        var default_pub = Boolean($scope.defaults.copy_notes_pub);
+        if (!angular.isArray(copy_list)) copy_list = [copy_list];
+
+        return $modal.open({
+            templateUrl: './cat/volcopy/t_copy_notes',
+            animation: true,
+            controller:
+                   ['$scope','$modalInstance',
+            function($scope , $modalInstance) {
+                $scope.focusNote = true;
+                $scope.note = {
+                    creator : egCore.auth.user().id(),
+                    title   : '',
+                    value   : '',
+                    pub     : default_pub,
+                };
+
+                $scope.require_initials = false;
+                egCore.org.settings([
+                    'ui.staff.require_initials.copy_notes'
+                ]).then(function(set) {
+                    $scope.require_initials = Boolean(set['ui.staff.require_initials.copy_notes']);
+                });
+
+                $scope.note_list = [];
+                if (copy_list.length == 1) {
+                    $scope.note_list = copy_list[0].notes();
+                }
+
+                $scope.ok = function(note) {
+
+                    if (note.initials) note.value += ' [' + note.initials + ']';
+                    angular.forEach(copy_list, function (cp) {
+                        var n = new egCore.idl.acpn();
+                        n.creator(note.creator);
+                        n.pub(note.pub);
+                        n.title(note.title);
+                        n.value(note.value);
+                        n.owning_copy(cp.id());
+                        cp.notes().push( n );
+                    });
+
+                    $modalInstance.close();
+                }
+
+                $scope.cancel = function($event) {
+                    $modalInstance.dismiss();
+                    $event.preventDefault();
+                }
+            }]
+        });
+    }
+
 }])
 
 .directive("egVolTemplate", function () {
@@ -1075,6 +1129,7 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
 
                 $scope.defaults = { // If defaults are not set at all, allow everything
                     statcats : true,
+                    copy_notes : true,
                     attributes : {
                         status : true,
                         loan_duration : true,

commit e089f5acf6626af27e0cc0263f3c977988c5e709
Author: Mike Rylander <mrylander at gmail.com>
Date:   Wed Sep 2 16:03:35 2015 -0400

    webstaff: Support fleshed updating of copy notes
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Cat/AssetCommon.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Cat/AssetCommon.pm
index 1f6075f..7b0ea44 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Cat/AssetCommon.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Cat/AssetCommon.pm
@@ -75,7 +75,8 @@ sub create_copy {
     $copy->call_number($vol->id);
     $class->fix_copy_price($copy);
 
-    $editor->create_asset_copy($copy) or return $editor->die_event;
+    my $cp = $editor->create_asset_copy($copy) or return $editor->die_event;
+    $copy->id($cp->id);
     return undef;
 }
 
@@ -223,6 +224,40 @@ sub update_copy_parts {
 
 
 
+sub update_copy_notes {
+    my($class, $editor, $copy) = @_;
+
+    return undef if $copy->isdeleted;
+    return undef unless $copy->ischanged or $copy->isnew;
+
+    my $evt;
+    my $incoming_notes = $copy->notes;
+
+    for my $incoming_note (@$incoming_notes) { 
+        next unless $incoming_note;
+
+        if ($incoming_note->isnew) {
+            my $new_note = Fieldmapper::asset::copy_note->new();
+            $new_note->owning_copy( $copy->id );
+            $new_note->pub( $incoming_note->pub );
+            $new_note->title( $incoming_note->title );
+            $new_note->value( $incoming_note->value );
+            $new_note->creator( $incoming_note->creator || $editor->requestor->id );
+            $incoming_note = $editor->create_asset_copy_note($new_note)
+                or return $editor->event;
+        } elif ($incoming_note->ischanged) {
+            $incoming_note = $editor->update_asset_copy_note($incoming_note)
+        } elif ($incoming_note->isdeleted) {
+            $incoming_note = $editor->delete_asset_copy_note($incoming_note->id)
+        }
+    
+    }
+
+    return undef;
+}
+
+
+
 sub update_copy {
     my($class, $editor, $override, $vol, $copy, $retarget_holds, $force_delete_empty_bib) = @_;
 
@@ -350,6 +385,7 @@ sub update_fleshed_copies {
         $copy->parts( $parts );
         # probably okay to use $delete_stats here for simplicity
         $evt = $class->update_copy_parts($editor, $copy, $delete_stats, $create_parts);
+        $evt = $class->update_copy_notes($editor, $copy);
         return $evt if $evt;
     }
 

commit 2d73d219844e8bb94656afe22e9576cb21edd5ec
Author: Mike Rylander <mrylander at gmail.com>
Date:   Wed Sep 2 11:46:28 2015 -0400

    webstaff: Dirty-data watcher stops unload when there is  unsaved data
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2 b/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2
index 2d071d9..b067b19 100644
--- a/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2
@@ -121,7 +121,7 @@
                  <eg-grid-menu-item handler="completeToWorking"
                   label="[% l('Edit Selected') %]"></eg-grid-menu-item>
         
-                 <eg-grid-menu-item handler="saveCompletedCopies"
+                 <eg-grid-menu-item handler="saveAndExit"
                   label="[% l('Save & Exit') %]"></eg-grid-menu-item>
         
                  <eg-grid-field label="[% l('Barcode') %]"     path='barcode' visible></eg-grid-field>
diff --git a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
index 71d3a2d..87f734a 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
@@ -499,8 +499,8 @@ function(egCore , $q) {
  * Edit controller!
  */
 .controller('EditCtrl', 
-       ['$scope','$q','$routeParams','$location','$timeout','egCore','egNet','egGridDataProvider','itemSvc',
-function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , egGridDataProvider , itemSvc) {
+       ['$scope','$q','$window','$routeParams','$location','$timeout','egCore','egNet','egGridDataProvider','itemSvc',
+function($scope , $q , $window , $routeParams , $location , $timeout , egCore , egNet , egGridDataProvider , itemSvc) {
 
     $scope.defaults = { // If defaults are not set at all, allow everything
         statcats : true,
@@ -546,6 +546,17 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
     $scope.fetchDefaults();
 
     $scope.dirty = false;
+    $scope.$watch('dirty',
+        function(newVal, oldVal) {
+            if (newVal && newVal != oldVal) {
+                $($window).on('beforeunload.edit', function(){
+                    return 'There is unsaved data!'
+                });
+            } else {
+                $($window).off('beforeunload.edit');
+            }
+        }
+    );
 
     $scope.show_vols = true;
     $scope.show_copies = true;
@@ -598,7 +609,13 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
                 if ($scope.workingGridControls && $scope.workingGridControls.selectedItems) {
                     angular.forEach(
                         $scope.workingGridControls.selectedItems(),
-                        function (cp) { cp[field](newval); cp.ischanged(1); }
+                        function (cp) {
+                            if (cp[field]() !== newval) {
+                                cp[field](newval);
+                                cp.ischanged(1);
+                                $scope.dirty = true;
+                            }
+                        }
                     );
                 }
             }
@@ -627,6 +644,8 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
                 angular.forEach(
                     $scope.workingGridControls.selectedItems(),
                     function (cp) {
+                        $scope.dirty = true;
+
                         cp.stat_cat_entries(
                             angular.forEach( cp.stat_cat_entries(), function (e) {
                                 if (e.stat_cat() == id) { // mark deleted
@@ -724,12 +743,16 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
                         angular.forEach(copies, function(cp) {
                             if (typeof $scope.batch.classification != 'undefined' && $scope.batch.classification != '')
                                 cp.call_number().label_class($scope.batch.classification);
+                                $scope.dirty = true;
                             if (typeof $scope.batch.prefix != 'undefined' && $scope.batch.prefix != '')
                                 cp.call_number().prefix($scope.batch.prefix);
+                                $scope.dirty = true;
                             if (typeof $scope.batch.label != 'undefined' && $scope.batch.label != '')
                                 cp.call_number().label($scope.batch.label);
+                                $scope.dirty = true;
                             if (typeof $scope.batch.suffix != 'undefined' && $scope.batch.suffix != '')
                                 cp.call_number().suffix($scope.batch.suffix);
+                                $scope.dirty = true;
                         });
                     });
                 });
@@ -997,7 +1020,7 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
         createSimpleUpdateWatcher('opac_visible');
         createSimpleUpdateWatcher('ref');
 
-        $scope.saveCompletedCopies = function () {
+        $scope.saveCompletedCopies = function (and_exit) {
             var cnHash = {};
             var perCnCopies = {};
             angular.forEach( $scope.completed_copies, function (cp) {
@@ -1008,6 +1031,7 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
                 } else {
                     perCnCopies[cn_id].push(cp);
                 }
+                cp.call_number(cn_id); // prevent loops in JSON-ification
             });
 
             angular.forEach(perCnCopies, function (v, k) {
@@ -1022,12 +1046,20 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
             egNet.request(
                 'open-ils.cat',
                 'open-ils.cat.asset.volume.fleshed.batch.update.override',
-                cnList, 1, { auto_merge_vols : 1, create_parts : 1 }
+                egCore.auth.token(), cnList, 1, { auto_merge_vols : 1, create_parts : 1 }
             ).then(function(update_count) {
                 alert(update_count + ' call numbers updated');
+                if (and_exit) {
+                    $scope.dirty = false;
+                    $timeout(function(){$window.close()});
+                }
             });
         }
 
+        $scope.saveAndExit = function () {
+            $scope.saveCompletedCopies(true);
+        }
+
     }
 
 }])
@@ -1038,8 +1070,8 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
         replace: true,
         template: '<div ng-include="'+"'/eg/staff/cat/volcopy/t_attr_edit'"+'"></div>',
         scope: { },
-        controller : ['$scope','itemSvc','egCore',
-            function ( $scope , itemSvc , egCore ) {
+        controller : ['$scope','$window','itemSvc','egCore',
+            function ( $scope , $window , itemSvc , egCore ) {
 
                 $scope.defaults = { // If defaults are not set at all, allow everything
                     statcats : true,
@@ -1076,6 +1108,18 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
                 $scope.fetchDefaults();
 
                 $scope.dirty = false;
+                $scope.$watch('dirty',
+                    function(newVal, oldVal) {
+                        if (newVal && newVal != oldVal) {
+                            $($window).on('beforeunload.template', function(){
+                                return 'There is unsaved template data!'
+                            });
+                        } else {
+                            $($window).off('beforeunload.template');
+                        }
+                    }
+                );
+
                 $scope.template_controls = true;
 
                 $scope.fetchTemplates = function () {
@@ -1129,6 +1173,8 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
             
                         egCore.hatch.setItem('cat.copy.templates', $scope.templates);
                         $scope.$parent.fetchTemplates();
+
+                        $scope.dirty = false;
                     }
                 }
             
@@ -1178,6 +1224,7 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
                             var newval = $scope.working.statcats[id];
                 
                             if (typeof newval != 'undefined') {
+                                $scope.dirty = true;
                                 if (angular.isObject(newval)) { // we'll use the pkey
                                     newval = newval.id();
                                 }
@@ -1204,6 +1251,7 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
                         }
                     });
                     $scope.working.circ_lib = undefined; // special
+                    $scope.dirty = false;
                 }
 
                 $scope.working = {};

commit 643e236766ffbfe2e7e5baaf7275620e506248e4
Author: Mike Rylander <mrylander at gmail.com>
Date:   Wed Sep 2 10:56:45 2015 -0400

    webstaff: Add call number attrs to copy templates
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2 b/Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2
index b38db52..d427704 100644
--- a/Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2
+++ b/Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2
@@ -30,6 +30,49 @@
         </div>
     </div>
 
+    <div class="row pad-vert" ng-if="template_controls && defaults.show_vol_template_controls">
+        <div class="row">
+            <div class="col-xs-12">
+                <h4 class="center-block">[% l('Volume Attributes') %]</h4>
+            </div>
+        </div>
+    </div>
+
+    <div class="row pad-vert" ng-if="template_controls && defaults.show_vol_template_controls">
+        <div class="col-md-1 bg-info">
+            <b>[% l('Classification') %]</b>
+        </div>
+        <div class="nullable col-md-2" ng-class="{'bg-success': working.callnumber.classification !== undefined}">
+            <select class="form-control" ng-model="working.callnumber.classification" ng-options="cl.id() as cl.name() for cl in classification_list">
+                <option value="">[% l('<NONE>') %]</option>
+            </select>
+        </div>
+        <div class="col-md-1 bg-info">
+            <b>[% l('Prefix') %]</b>
+        </div>
+        <div class="nullable col-xs-2" ng-class="{'bg-success': working.callnumber.prefix !== undefined}">
+            <select class="form-control" ng-model="working.callnumber.prefix" ng-options="p.id() as p.label() for p in prefix_list">
+                <option value="">[% l('<NONE>') %]</option>
+            </select>
+        </div>
+        <div class="col-md-1 bg-info">
+            <b>[% l('Suffix') %]</b>
+        </div>
+        <div class="nullable col-md-2" ng-class="{'bg-success': working.callnumber.suffix !== undefined}">
+            <select class="form-control" ng-model="working.callnumber.suffix" ng-options="s.id() as s.label() for s in suffix_list">
+                <option value="">[% l('<NONE>') %]</option>
+            </select>
+        </div>
+    </div>
+
+    <div class="row pad-vert" ng-if="template_controls && defaults.show_vol_template_controls">
+        <div class="row">
+            <div class="col-xs-12">
+                <h4 class="center-block">[% l('Copy Attributes') %]</h4>
+            </div>
+        </div>
+    </div>
+
     <div class="row pad-vert"></div>
 
     <div class="row bg-info">
diff --git a/Open-ILS/src/templates/staff/cat/volcopy/t_defaults.tt2 b/Open-ILS/src/templates/staff/cat/volcopy/t_defaults.tt2
index 44c28d8..f589139 100644
--- a/Open-ILS/src/templates/staff/cat/volcopy/t_defaults.tt2
+++ b/Open-ILS/src/templates/staff/cat/volcopy/t_defaults.tt2
@@ -4,12 +4,24 @@
             <div class="row">
                 <div class="col-xs-12">
                     <h4>[% l('Volume/Copy Detail defaults') %]</h4>
+                </div>
+            </div>
+            <div class="row">
+                <div class="col-xs-12">
                     <label>
                         <input type="checkbox" ng-change="saveDefaults()" ng-model="defaults.always_volumes"/>
                         [% l('Always display Volume/Copy Detail pane') %]
                     </label>
                 </div>
             </div>
+            <div class="row">
+                <div class="col-xs-12">
+                    <label>
+                        <input type="checkbox" ng-change="saveDefaults()" ng-model="defaults.show_vol_template_controls"/>
+                        [% l('Allow Call Number attributes in Copy Templates') %]
+                    </label>
+                </div>
+            </div>
 
             <div class="row">
                 <div class="col-xs-12">
diff --git a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
index cfe93f9..71d3a2d 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
@@ -201,8 +201,8 @@ function(egCore , $q) {
             '</div>',
 
         scope: { copy: "=", callNumber: "=", index: "@" },
-        controller : ['$scope','itemSvc',
-            function ( $scope , itemSvc ) {
+        controller : ['$scope','itemSvc','egCore',
+            function ( $scope , itemSvc , egCore ) {
                 $scope.new_part_id = 0;
 
                 $scope.nextBarcode = function (i) {
@@ -247,7 +247,7 @@ function(egCore , $q) {
                 itemSvc.get_parts($scope.callNumber.record()).then(function(list){
                     $scope.part_list = list;
                     angular.forEach(list, function(p){ $scope.parts.push(p.label()) });
-                    $scope.parts = angluar.copy($scope.parts);
+                    $scope.parts = angular.copy($scope.parts);
                 });
 
             }
@@ -691,8 +691,15 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
                     $scope.working[k] = angular.copy(v);
                 } else {
                     angular.forEach(v, function (sv,sk) {
-                        $scope.working[k][sk] = angular.copy(sv);
-                        if (k == 'statcats') $scope.statcatUpdate(sk);
+                        if (k == 'callnumber') {
+                            angular.forEach(v, function (cnv,cnk) {
+                                $scope.batch[cnk] = cnv;
+                            });
+                            $scope.applyBatchCNValues();
+                        } else {
+                            $scope.working[k][sk] = angular.copy(sv);
+                            if (k == 'statcats') $scope.statcatUpdate(sk);
+                        }
                     });
                 }
             });
@@ -1012,11 +1019,13 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
                 cnList.push(v);
             });
 
-            egCore.net.request(
+            egNet.request(
                 'open-ils.cat',
                 'open-ils.cat.asset.volume.fleshed.batch.update.override',
                 cnList, 1, { auto_merge_vols : 1, create_parts : 1 }
-            );
+            ).then(function(update_count) {
+                alert(update_count + ' call numbers updated');
+            });
         }
 
     }
@@ -1263,6 +1272,25 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
                 createSimpleUpdateWatcher('mint_condition');
                 createSimpleUpdateWatcher('opac_visible');
                 createSimpleUpdateWatcher('ref');
+
+                $scope.suffix_list = [];
+                itemSvc.get_suffixes(egCore.auth.user().ws_ou()).then(function(list){
+                    $scope.suffix_list = list;
+                });
+
+                $scope.prefix_list = [];
+                itemSvc.get_prefixes(egCore.auth.user().ws_ou()).then(function(list){
+                    $scope.prefix_list = list;
+                });
+
+                $scope.classification_list = [];
+                itemSvc.get_classifications().then(function(list){
+                    $scope.classification_list = list;
+                });
+
+                createSimpleUpdateWatcher('working.callnumber.classification');
+                createSimpleUpdateWatcher('working.callnumber.prefix');
+                createSimpleUpdateWatcher('working.callnumber.suffix');
             }
         ]
     }

commit ee918900a91d239d0b58f190c5b3984408326982
Author: Mike Rylander <mrylander at gmail.com>
Date:   Tue Sep 1 15:17:01 2015 -0400

    webstaff: Track and update part info
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2 b/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2
index b067b19..2d071d9 100644
--- a/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2
@@ -121,7 +121,7 @@
                  <eg-grid-menu-item handler="completeToWorking"
                   label="[% l('Edit Selected') %]"></eg-grid-menu-item>
         
-                 <eg-grid-menu-item handler="saveAndExit"
+                 <eg-grid-menu-item handler="saveCompletedCopies"
                   label="[% l('Save & Exit') %]"></eg-grid-menu-item>
         
                  <eg-grid-field label="[% l('Barcode') %]"     path='barcode' visible></eg-grid-field>
diff --git a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
index 3dc59fc..cfe93f9 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
@@ -212,21 +212,26 @@ function(egCore , $q) {
                 $scope.updateBarcode = function () { $scope.copy.barcode($scope.barcode); $scope.copy.ischanged(1); };
                 $scope.updateCopyNo = function () { $scope.copy.copy_number($scope.copy_number); $scope.copy.ischanged(1); };
                 $scope.updatePart = function () {
-                    var p = $scope.part_list.filter(function (x) {
-                        return x.label() == $scope.part
-                    });
-                    if (p.length > 0) { // preexisting part
-                        $scope.copy.parts(p)
-                    } else { // create one...
-                        var part = new egCore.idl.bmp();
-                        part.id( --$scope.new_part_id );
-                        part.isnew( true );
-                        part.label( $scope.part );
-                        part.record( $scope.callNumber.owning_lib() );
-                        $scope.copy.parts([part]);
-                        $scope.copy.ischanged(1);
+                    if ($scope.part) {
+                        var p = $scope.part_list.filter(function (x) {
+                            return x.label() == $scope.part
+                        });
+                        if (p.length > 0) { // preexisting part
+                            $scope.copy.parts(p)
+                        } else { // create one...
+                            var part = new egCore.idl.bmp();
+                            part.id( --$scope.new_part_id );
+                            part.isnew( true );
+                            part.label( $scope.part );
+                            part.record( $scope.callNumber.owning_lib() );
+                            $scope.copy.parts([part]);
+                            $scope.copy.ischanged(1);
+                        }
+                    } else {
+                        $scope.copy.parts([]);
                     }
                 }
+                $scope.$watch('part', $scope.updatePart);
 
                 $scope.barcode = $scope.copy.barcode();
                 $scope.copy_number = $scope.copy.copy_number();
@@ -242,6 +247,7 @@ function(egCore , $q) {
                 itemSvc.get_parts($scope.callNumber.record()).then(function(list){
                     $scope.part_list = list;
                     angular.forEach(list, function(p){ $scope.parts.push(p.label()) });
+                    $scope.parts = angluar.copy($scope.parts);
                 });
 
             }
@@ -984,6 +990,35 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
         createSimpleUpdateWatcher('opac_visible');
         createSimpleUpdateWatcher('ref');
 
+        $scope.saveCompletedCopies = function () {
+            var cnHash = {};
+            var perCnCopies = {};
+            angular.forEach( $scope.completed_copies, function (cp) {
+                var cn_id = cp.call_number().id();
+                if (!cnHash[cn_id]) {
+                    cnHash[cn_id] = cp.call_number();
+                    perCnCopies[cn_id] = [cp];
+                } else {
+                    perCnCopies[cn_id].push(cp);
+                }
+            });
+
+            angular.forEach(perCnCopies, function (v, k) {
+                cnHash[k].copies(v);
+            });
+
+            cnList = [];
+            angular.forEach(cnHash, function (v, k) {
+                cnList.push(v);
+            });
+
+            egCore.net.request(
+                'open-ils.cat',
+                'open-ils.cat.asset.volume.fleshed.batch.update.override',
+                cnList, 1, { auto_merge_vols : 1, create_parts : 1 }
+            );
+        }
+
     }
 
 }])

commit 4805efa9b793a0ac7021071a95815bda15d758c7
Author: Mike Rylander <mrylander at gmail.com>
Date:   Tue Sep 1 15:16:31 2015 -0400

    webstaff: Allow automatic part creation
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Cat.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Cat.pm
index 3193219..4a5d1b3 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Cat.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Cat.pm
@@ -721,7 +721,7 @@ __PACKAGE__->register_method(
 
 
 sub fleshed_copy_update {
-    my( $self, $conn, $auth, $copies, $delete_stats, $oargs ) = @_;
+    my( $self, $conn, $auth, $copies, $delete_stats, $oargs, $create_parts ) = @_;
     return 1 unless ref $copies;
     my( $reqr, $evt ) = $U->checkses($auth);
     return $evt if $evt;
@@ -733,7 +733,7 @@ sub fleshed_copy_update {
     }
     my $retarget_holds = [];
     $evt = OpenILS::Application::Cat::AssetCommon->update_fleshed_copies(
-        $editor, $oargs, undef, $copies, $delete_stats, $retarget_holds, undef);
+        $editor, $oargs, undef, $copies, $delete_stats, $retarget_holds, undef, $create_parts);
 
     if( $evt ) { 
         $logger->info("fleshed copy update failed with event: ".OpenSRF::Utils::JSON->perl2JSON($evt));
@@ -865,6 +865,7 @@ sub fleshed_volume_update {
     my $editor = new_editor( requestor => $reqr, xact => 1 );
     my $retarget_holds = [];
     my $auto_merge_vols = $options->{auto_merge_vols};
+    my $create_parts = $options->{create_parts};
 
     for my $vol (@$volumes) {
         $logger->info("vol-update: investigating volume ".$vol->id);
@@ -908,7 +909,7 @@ sub fleshed_volume_update {
         if( $copies and @$copies and !$vol->isdeleted ) {
             $_->call_number($vol->id) for @$copies;
             $evt = $assetcom->update_fleshed_copies(
-                $editor, $oargs, $vol, $copies, $delete_stats, $retarget_holds, undef);
+                $editor, $oargs, $vol, $copies, $delete_stats, $retarget_holds, undef, $create_parts);
             return $evt if $evt;
         }
     }
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Cat/AssetCommon.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Cat/AssetCommon.pm
index 4d78cb5..1f6075f 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Cat/AssetCommon.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Cat/AssetCommon.pm
@@ -160,7 +160,7 @@ sub update_copy_stat_entries {
 # authoritative list for the copy. existing part maps not targeting
 # these parts will be deleted from the DB
 sub update_copy_parts {
-    my($class, $editor, $copy, $delete_maps) = @_;
+    my($class, $editor, $copy, $delete_maps, $create_parts) = @_;
 
     return undef if $copy->isdeleted;
     return undef unless $copy->ischanged or $copy->isnew;
@@ -197,6 +197,15 @@ sub update_copy_parts {
 
         # if this link already exists in the DB, don't attempt to re-create it
         next if( grep{$_->part == $incoming_part->id} @$maps );
+
+        if ($incoming_part->isnew) {
+            next unless $create_parts;
+            my $new_part = Fieldmapper::biblio::monograph_part->new();
+            $new_part->record( $incoming_part->record );
+            $new_part->label( $incoming_part->label );
+            $incoming_part = $editor->create_biblio_monograph_part($new_part)
+                or return $editor->event;
+        }
     
         my $new_map = Fieldmapper::asset::copy_part_map->new();
 
@@ -284,7 +293,7 @@ sub check_hold_retarget {
 
 # this does the actual work
 sub update_fleshed_copies {
-    my($class, $editor, $override, $vol, $copies, $delete_stats, $retarget_holds, $force_delete_empty_bib) = @_;
+    my($class, $editor, $override, $vol, $copies, $delete_stats, $retarget_holds, $force_delete_empty_bib, $create_parts) = @_;
 
     $override = { all => 1 } if($override && !ref $override);
     $override = { all => 0 } if(!ref $override);
@@ -340,7 +349,7 @@ sub update_fleshed_copies {
         $evt = $class->update_copy_stat_entries($editor, $copy, $delete_stats);
         $copy->parts( $parts );
         # probably okay to use $delete_stats here for simplicity
-        $evt = $class->update_copy_parts($editor, $copy, $delete_stats);
+        $evt = $class->update_copy_parts($editor, $copy, $delete_stats, $create_parts);
         return $evt if $evt;
     }
 

commit 14554202c66931a2db463202cd5cfe1680174313
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Tue Sep 1 18:44:11 2015 +0000

    webstaff: check that control set loaded before loading record into MARC editor
    
    This seems hacky... but it also seems to work.
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js b/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
index e36fac8..f45376a 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
@@ -1027,9 +1027,16 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
                     }
                 );
 
-                if ($scope.recordId || $scope.marcXml) {
-                    loadRecord();
-                }
+                var unregister = $scope.$watch(function() {
+                    return egTagTable.initialized();
+                }, function(val) {
+                    if (val) {
+                        unregister();
+                        if ($scope.recordId || $scope.marcXml) {
+                            loadRecord();
+                        }
+                    }
+                });
 
                 $scope.mangle_005 = function () {
                     var now = new Date();
diff --git a/Open-ILS/web/js/ui/default/staff/cat/services/tagtable.js b/Open-ILS/web/js/ui/default/staff/cat/services/tagtable.js
index 6b40c9c..f32b2de 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/services/tagtable.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/services/tagtable.js
@@ -15,10 +15,15 @@ function($q,   egCore,   egAuth) {
         ff_pos_map : { },
         ff_value_map : { },
         authority_control_set : {
+            _remote_loaded : false,
             _controlsets : [ ]
         }
     };
 
+    service.initialized = function() {
+        return service.authority_control_set._remote_loaded;
+    }
+
     // allow 'bre' and 'biblio' to be synonyms, etc.
     service.normalizeRecordType = function(recordType) {
         if (recordType === 'sre') {

commit b85c76647e5dd8dda64ab663e666bc9cd08da80f
Author: Mike Rylander <mrylander at gmail.com>
Date:   Tue Sep 1 14:03:26 2015 -0400

    webstaff: volume/copy editor improvements
    
    * Protect copies and vols from being removed via "count" fields
    * avoid missing statcat errors
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
index bdc86b3..3dc59fc 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
@@ -393,17 +393,19 @@ function(egCore , $q) {
                         $scope.allcopies.push( cp );
                     }
 
-                    var how_many = $scope.copies.length - $scope.copy_count;
-                    if (how_many > 0) {
-                        var dead = $scope.copies.splice($scope.copy_count,how_many);
-                        $scope.callNumber.copies($scope.copies);
-
-                        // Trimming the global list is a bit more tricky
-                        angular.forEach( dead, function (d) {
-                            angular.forEach( $scope.allcopies, function (l, i) { 
-                                if (l === d) $scope.allcopies.splice(i,1);
+                    if ($scope.copy_count >= $scope.orig_copy_count) {
+                        var how_many = $scope.copies.length - $scope.copy_count;
+                        if (how_many > 0) {
+                            var dead = $scope.copies.splice($scope.copy_count,how_many);
+                            $scope.callNumber.copies($scope.copies);
+
+                            // Trimming the global list is a bit more tricky
+                            angular.forEach( dead, function (d) {
+                                angular.forEach( $scope.allcopies, function (l, i) { 
+                                    if (l === d) $scope.allcopies.splice(i,1);
+                                });
                             });
-                        });
+                        }
                     }
                 }
 
@@ -464,7 +466,7 @@ function(egCore , $q) {
                             $scope.struct[cn.id()] = [cp];
                             $scope.allcopies.push(cp);
                         }
-                    } else if (n < o) { // removing
+                    } else if (n < o && n >= $scope.orig_cn_count) { // removing
                         var how_many = o - n;
                         var list = Object
                                 .keys($scope.struct)
@@ -658,20 +660,6 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
         }
     }
 
-    $scope.applyTemplate = function (n) {
-        angular.forEach($scope.templates[n], function (v,k) {
-            if (!angular.isObject(v)) {
-                $scope.working[k] = angular.copy(v);
-            } else {
-                angular.forEach(v, function (sv,sk) {
-                    $scope.working[k][sk] = angular.copy(sv);
-                    if (k == 'statcats') $scope.statcatUpdate(sk);
-                });
-            }
-        });
-        $scope.template_name = '';
-    }
-
     var dataKey = $routeParams.dataKey;
     console.debug('dataKey: ' + dataKey);
 
@@ -690,7 +678,21 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
             });
         }
         $scope.fetchTemplates();
- 
+
+         $scope.applyTemplate = function (n) {
+            angular.forEach($scope.templates[n], function (v,k) {
+                if (!angular.isObject(v)) {
+                    $scope.working[k] = angular.copy(v);
+                } else {
+                    angular.forEach(v, function (sv,sk) {
+                        $scope.working[k][sk] = angular.copy(sv);
+                        if (k == 'statcats') $scope.statcatUpdate(sk);
+                    });
+                }
+            });
+            $scope.template_name = '';
+        }
+
         $scope.copytab = 'working';
         $scope.tab = 'edit';
         $scope.summaryRecord = null;
@@ -830,15 +832,17 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
                     var value_hash = {};
                     var none = false;
                     angular.forEach(item_list, function (item) {
-                        if (item.stat_cat_entries().length > 0) {
-                            var right_sc = item.stat_cat_entries().filter(function (e) {
-                                return e.stat_cat() == sc.id() && !Boolean(e.isdeleted());
-                            });
+                        if (item.stat_cat_entries()) {
+                            if (item.stat_cat_entries().length > 0) {
+                                var right_sc = item.stat_cat_entries().filter(function (e) {
+                                    return e.stat_cat() == sc.id() && !Boolean(e.isdeleted());
+                                });
 
-                            if (right_sc.length > 0) {
-                                value_hash[right_sc[0].stat_cat_entry()] = right_sc[0].stat_cat_entry();
-                            } else {
-                                none = true;
+                                if (right_sc.length > 0) {
+                                    value_hash[right_sc[0].stat_cat_entry()] = right_sc[0].stat_cat_entry();
+                                } else {
+                                    none = true;
+                                }
                             }
                         } else {
                             none = true;

commit c9efa31128635fdc7457a67a5eb25149637d31f7
Author: Mike Rylander <mrylander at gmail.com>
Date:   Tue Sep 1 13:52:41 2015 -0400

    webstaff: Close dropdown on select
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/services/ui.js b/Open-ILS/web/js/ui/default/staff/services/ui.js
index 3217996..4960317 100644
--- a/Open-ILS/web/js/ui/default/staff/services/ui.js
+++ b/Open-ILS/web/js/ui/default/staff/services/ui.js
@@ -240,6 +240,7 @@ function($modal, $interpolate) {
 
                 $scope.changeValue = function (newVal) {
                     $scope.selected = newVal;
+                    $scope.isopen = false;
                 }
 
             }

commit 45c87bcecca7f73bd27bb5b8190672c7cadbb174
Author: Mike Rylander <mrylander at gmail.com>
Date:   Tue Sep 1 13:50:13 2015 -0400

    webstaff: No need for focus-me
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/services/ui.js b/Open-ILS/web/js/ui/default/staff/services/ui.js
index 0c240c2..3217996 100644
--- a/Open-ILS/web/js/ui/default/staff/services/ui.js
+++ b/Open-ILS/web/js/ui/default/staff/services/ui.js
@@ -217,7 +217,7 @@ function($modal, $interpolate) {
         },
         template:
             '<div class="input-group">'+
-                '<input type="text" class="form-control" ng-model="selected" focus-me="always" ng-change="makeOpen()">'+
+                '<input type="text" class="form-control" ng-model="selected" ng-change="makeOpen()">'+
                 '<div class="input-group-btn" dropdown ng-class="{open:isopen}">'+
                     '<button type="button" class="btn btn-default dropdown-toggle"><span class="caret"></span></button>'+
                     '<ul class="dropdown-menu dropdown-menu-right">'+

commit ea38b91ea173f009aef4875a9d7732847bf75673
Author: Mike Rylander <mrylander at gmail.com>
Date:   Tue Sep 1 13:03:44 2015 -0400

    webstaff: Clear template name after application so another can be selected
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
index 68aab84..bdc86b3 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
@@ -669,6 +669,7 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
                 });
             }
         });
+        $scope.template_name = '';
     }
 
     var dataKey = $routeParams.dataKey;
@@ -1049,6 +1050,7 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
                             });
                         }
                     });
+                    $scope.template_name = '';
                 }
 
                 $scope.deleteTemplate = function (n) {

commit 59b8da708b411997900c21fc8f551d8efb2ffe08
Author: Mike Rylander <mrylander at gmail.com>
Date:   Tue Sep 1 13:03:13 2015 -0400

    webstaff: Show filtered list on user input
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/services/ui.js b/Open-ILS/web/js/ui/default/staff/services/ui.js
index aa5d0b7..0c240c2 100644
--- a/Open-ILS/web/js/ui/default/staff/services/ui.js
+++ b/Open-ILS/web/js/ui/default/staff/services/ui.js
@@ -217,16 +217,26 @@ function($modal, $interpolate) {
         },
         template:
             '<div class="input-group">'+
-                '<input type="text" class="form-control" ng-model="selected">'+
-                '<div class="input-group-btn" dropdown>'+
+                '<input type="text" class="form-control" ng-model="selected" focus-me="always" ng-change="makeOpen()">'+
+                '<div class="input-group-btn" dropdown ng-class="{open:isopen}">'+
                     '<button type="button" class="btn btn-default dropdown-toggle"><span class="caret"></span></button>'+
                     '<ul class="dropdown-menu dropdown-menu-right">'+
-                        '<li ng-repeat="item in list"><a href ng-click="changeValue(item)">{{item}}</a></li>'+
+                        '<li ng-repeat="item in list|filter:selected"><a href ng-click="changeValue(item)">{{item}}</a></li>'+
                     '</ul>'+
                 '</div>'+
             '</div>',
-        controller: ['$scope',
-            function($scope) {
+        controller: ['$scope','$filter',
+            function( $scope , $filter) {
+
+                $scope.always = true;
+                $scope.isopen = false;
+
+                $scope.makeOpen = function () {
+                    return $scope.isopen = $filter('filter')(
+                        $scope.list,
+                        $scope.selected
+                    ).length > 0 && $scope.selected.length > 0;
+                }
 
                 $scope.changeValue = function (newVal) {
                     $scope.selected = newVal;

commit 2f33494db6ee17fed89b4fa48772cbb51f6011a4
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Tue Sep 1 16:51:40 2015 +0000

    webstaff: map see from/see also from correctly
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/share/t_authority_browser.tt2 b/Open-ILS/src/templates/staff/cat/share/t_authority_browser.tt2
index 1cd4ef2..b529722 100644
--- a/Open-ILS/src/templates/staff/cat/share/t_authority_browser.tt2
+++ b/Open-ILS/src/templates/staff/cat/share/t_authority_browser.tt2
@@ -37,7 +37,7 @@
             <li ng-repeat="seealso in main.seealso_headings">
                 <div class="form-inline">
                     <button class="btn btn-xs btn-warning" ng-click="applyHeading({hdg : seealso.headingField})">[% l('Apply') %]</button>
-                    [% l('See also: [_1]', '{{seealso.heading}}') %]
+                    [% l('See also from: [_1]', '{{seealso.heading}}') %]
                     (<span style="font-family: 'Lucida Console', Monaco, monospace;">
                         <span ng-repeat="sf in seealso.headingField.subfields">
                             <span class="marcsfcodedelimiter">‡{{sf.0}}</span> {{sf.1}}
diff --git a/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js b/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
index 415f32d..e36fac8 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
@@ -1355,8 +1355,8 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
                                 auth_org = rec.field('003').data;
                             }
                             var headingField = rec.field('1..');
-                            var seeAlsos = rec.field('4..', true);
-                            var seeFroms = rec.field('5..', true);
+                            var seeFroms = rec.field('4..', true);
+                            var seeAlsos = rec.field('5..', true);
 
                             var main_heading = {
                                 authority_id : authId,

commit db9b0dff87cc8ad5a2ff97b60f418eee77c2e54d
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Tue Sep 1 16:32:35 2015 +0000

    webstaff: add another set of navigation buttons to headings chooser
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/share/t_authority_browser.tt2 b/Open-ILS/src/templates/staff/cat/share/t_authority_browser.tt2
index ec7428c..1cd4ef2 100644
--- a/Open-ILS/src/templates/staff/cat/share/t_authority_browser.tt2
+++ b/Open-ILS/src/templates/staff/cat/share/t_authority_browser.tt2
@@ -1,5 +1,16 @@
 <div>
 <div>
+    <button class="btn btn-default btn-sm" ng-click="page = 0">
+         [% l('Start') %]
+    </button>
+    <button class="btn btn-default btn-sm" ng-class="{disabled : !page}" ng-click="page = page - 1">
+         [% l('Previous') %]
+    </button>
+    <button class="btn btn-default btn-sm" ng-click="page = page + 1">
+         [% l('Next') %]
+    </button>
+</div>
+<div>
 <ul>
     <li ng-repeat="main in main_headings">
         <div class="form-inline">

commit acce2de94fa82da4608faf4ec36f922df344a6aa
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Tue Sep 1 16:30:32 2015 +0000

    webstaff: tweak headings chooser to take less vertical space
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/share/t_authority_browser.tt2 b/Open-ILS/src/templates/staff/cat/share/t_authority_browser.tt2
index f7f2043..ec7428c 100644
--- a/Open-ILS/src/templates/staff/cat/share/t_authority_browser.tt2
+++ b/Open-ILS/src/templates/staff/cat/share/t_authority_browser.tt2
@@ -2,36 +2,36 @@
 <div>
 <ul>
     <li ng-repeat="main in main_headings">
-        {{main.heading}}
-        <div class="row form-inline">
+        <div class="form-inline">
             <button class="btn btn-xs btn-primary" ng-click="applyHeading({hdg : main.headingField})" >[% l('Apply') %]</button>
-            <span style="font-family: 'Lucida Console', Monaco, monospace;">
+            {{main.heading}}
+            (<span style="font-family: 'Lucida Console', Monaco, monospace;">
                 <span ng-repeat="sf in main.headingField.subfields">
                     <span class="marcsfcodedelimiter">‡{{sf.0}}</span> {{sf.1}}
                 </span>
-            </span>
+            </span>)
         </div>
         <ul ng-if="main.seefrom_headings || main.seealso_headings">
             <li ng-repeat="seefrom in main.seefrom_headings">
-                [% l('See from: [_1]', '{{seefrom.heading}}') %]
-                <div class="row form-inline">
+                <div class="form-inline">
                     <button class="btn btn-xs btn-warning" ng-click="applyHeading({hdg : seefrom.headingField})">[% l('Apply') %]</button>
-                    <span style="font-family: 'Lucida Console', Monaco, monospace;">
+                    [% l('See from: [_1]', '{{seefrom.heading}}') %]
+                    (<span style="font-family: 'Lucida Console', Monaco, monospace;">
                         <span ng-repeat="sf in seefrom.headingField.subfields">
                             <span class="marcsfcodedelimiter">‡{{sf.0}}</span> {{sf.1}}
                         </span>
-                    </span>
+                    </span>)
                 </div>
             </li>
             <li ng-repeat="seealso in main.seealso_headings">
-                [% l('See also: [_1]', '{{seealso.heading}}') %]
-                <div class="row form-inline">
+                <div class="form-inline">
                     <button class="btn btn-xs btn-warning" ng-click="applyHeading({hdg : seealso.headingField})">[% l('Apply') %]</button>
-                    <span style="font-family: 'Lucida Console', Monaco, monospace;">
+                    [% l('See also: [_1]', '{{seealso.heading}}') %]
+                    (<span style="font-family: 'Lucida Console', Monaco, monospace;">
                         <span ng-repeat="sf in seealso.headingField.subfields">
                             <span class="marcsfcodedelimiter">‡{{sf.0}}</span> {{sf.1}}
                         </span>
-                    </span>
+                    </span>)
                 </div>
             </li>
         </ul>
diff --git a/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js b/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
index 97574cc..415f32d 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
@@ -397,7 +397,7 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
                     var args = { changed : false };
                     $modal.open({
                         templateUrl: './cat/share/t_authority_link_dialog',
-                        size: 'md',
+                        size: 'lg',
                         controller: ['$scope', '$modalInstance', function($scope, $modalInstance) {
                             $scope.controlSet = cs;
                             $scope.bibField = fieldCopy;
@@ -1306,7 +1306,7 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
             function ($scope , $http) {
 
                 $scope.page = 0;
-                $scope.limit = 10;
+                $scope.limit = 5;
                 $scope.main_headings = [];
 
                 function getHeadingString(headingField) {

commit 557ecb97fb98bc88980db94cf223d9d18aed7e69
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Tue Sep 1 04:29:57 2015 +0000

    webstaff: implement a headings browser and chooser in authority linker
    
    The authority linker dialog in the web staff interface now
    knows how to browse list of available headings.
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/share/t_authority_browser.tt2 b/Open-ILS/src/templates/staff/cat/share/t_authority_browser.tt2
new file mode 100644
index 0000000..f7f2043
--- /dev/null
+++ b/Open-ILS/src/templates/staff/cat/share/t_authority_browser.tt2
@@ -0,0 +1,52 @@
+<div>
+<div>
+<ul>
+    <li ng-repeat="main in main_headings">
+        {{main.heading}}
+        <div class="row form-inline">
+            <button class="btn btn-xs btn-primary" ng-click="applyHeading({hdg : main.headingField})" >[% l('Apply') %]</button>
+            <span style="font-family: 'Lucida Console', Monaco, monospace;">
+                <span ng-repeat="sf in main.headingField.subfields">
+                    <span class="marcsfcodedelimiter">‡{{sf.0}}</span> {{sf.1}}
+                </span>
+            </span>
+        </div>
+        <ul ng-if="main.seefrom_headings || main.seealso_headings">
+            <li ng-repeat="seefrom in main.seefrom_headings">
+                [% l('See from: [_1]', '{{seefrom.heading}}') %]
+                <div class="row form-inline">
+                    <button class="btn btn-xs btn-warning" ng-click="applyHeading({hdg : seefrom.headingField})">[% l('Apply') %]</button>
+                    <span style="font-family: 'Lucida Console', Monaco, monospace;">
+                        <span ng-repeat="sf in seefrom.headingField.subfields">
+                            <span class="marcsfcodedelimiter">‡{{sf.0}}</span> {{sf.1}}
+                        </span>
+                    </span>
+                </div>
+            </li>
+            <li ng-repeat="seealso in main.seealso_headings">
+                [% l('See also: [_1]', '{{seealso.heading}}') %]
+                <div class="row form-inline">
+                    <button class="btn btn-xs btn-warning" ng-click="applyHeading({hdg : seealso.headingField})">[% l('Apply') %]</button>
+                    <span style="font-family: 'Lucida Console', Monaco, monospace;">
+                        <span ng-repeat="sf in seealso.headingField.subfields">
+                            <span class="marcsfcodedelimiter">‡{{sf.0}}</span> {{sf.1}}
+                        </span>
+                    </span>
+                </div>
+            </li>
+        </ul>
+    </li>
+</ul>
+</div>
+<div>
+    <button class="btn btn-default btn-sm" ng-click="page = 0">
+         [% l('Start') %]
+    </button>
+    <button class="btn btn-default btn-sm" ng-class="{disabled : !page}" ng-click="page = page - 1">
+         [% l('Previous') %]
+    </button>
+    <button class="btn btn-default btn-sm" ng-click="page = page + 1">
+         [% l('Next') %]
+    </button>
+</div>
+</div>
diff --git a/Open-ILS/src/templates/staff/cat/share/t_authority_linker.tt2 b/Open-ILS/src/templates/staff/cat/share/t_authority_linker.tt2
index ac15751..6dd5731 100644
--- a/Open-ILS/src/templates/staff/cat/share/t_authority_linker.tt2
+++ b/Open-ILS/src/templates/staff/cat/share/t_authority_linker.tt2
@@ -8,11 +8,15 @@
     </div>
     <div class="row form-inline">
         [% l('Create new authority from this field') %]
-        <button class="btn btn-primary button-sm" ng-click="createAuthorityFromBib()">
+        <button class="btn btn-primary btn-sm" ng-click="createAuthorityFromBib()">
              [% l('Immediately') %]
         </button>
-        <button class="btn btn-primary button-sm" ng-click="createAuthorityFromBib(true)">
+        <button class="btn btn-primary btn-sm" ng-click="createAuthorityFromBib(true)">
              [% l('Create and edit') %]
         </button>
     </div>
+    <hr>
+    <div class="row">
+        <eg-marc-edit-authority-browser search-string="searchStr" control-set="controlSet" axis="axis" apply-heading="applyHeading(hdg)" />
+    </div>
 </div>
diff --git a/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js b/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
index 2ed3339..97574cc 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
@@ -1111,19 +1111,32 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
         controller: ['$scope','$modal','egCore','egAuth',
             function ($scope , $modal,  egCore,  egAuth) {
 
+                $scope.searchStr = '';
                 var cni = egCore.env.aous['cat.marc_control_number_identifier'] ||
                   'Set cat.marc_control_number_identifier in Library Settings';
-                
+
+                var axis_list = $scope.controlSet.bibFieldBrowseAxes($scope.bibField.tag);
+                $scope.axis = axis_list[0];
+
                 $scope._controlled_sf_list = {};
+                $scope._controlled_auth_sf_list = {};
                 var found_acs = [];
                 angular.forEach($scope.controlSet.controlSetList(), function(acs_id) {
                     if ($scope.controlSet.controlSet(acs_id).control_map[$scope.bibField.tag])
                         found_acs.push(acs_id);
                 });
                 if (found_acs.length) {
-                     angular.forEach(
-                        $scope.controlSet.controlSet(found_acs[0]).control_map[$scope.bibField.tag],                         function(sf) {
-                            $scope._controlled_sf_list[ sf[$scope.bibField.tag] ] = 1;
+                     angular.forEach($scope.controlSet.controlSet(found_acs[0]).control_map[$scope.bibField.tag],
+                        function(value, sf_label) {
+                            $scope._controlled_sf_list[ sf_label ] = 1;
+                            angular.forEach($scope.controlSet.controlSet(found_acs[0]).control_map[$scope.bibField.tag][sf_label],
+                                function(auth_sf, auth_tag) {
+                                    if (!$scope._controlled_auth_sf_list[auth_tag]) {
+                                        $scope._controlled_auth_sf_list[auth_tag] = { };
+                                    }
+                                    $scope._controlled_auth_sf_list[auth_tag][auth_sf] = 1;
+                                }
+                            );
                         }
                     )
                 }
@@ -1150,6 +1163,25 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
                     });
                     return source_f;
                 }
+                $scope.getSearchString = function() {
+                    var source_f = $scope.summarizeField();
+                    var values = [];
+                    angular.forEach(source_f.subfields, function(val) {
+                        values.push(val[1]);
+                    });
+                    return values.join(' ');
+                }
+                $scope.searchStr = $scope.getSearchString();
+                $scope.$watch(function() {
+                    var ct = 0;
+                    angular.forEach($scope.bibField.subfields, function(sf) {
+                        if (sf.selected) ct++
+                        });
+                    return ct;
+                },
+                function(newVal, oldVal) {
+                    $scope.searchStr = $scope.getSearchString();
+                });
 
                 $scope.updateSubfieldZero = function(value) {
                     $scope.changed = true;
@@ -1159,6 +1191,64 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
                     ]);
                 };
 
+                $scope.applyHeading = function(headingField) {
+                    // TODO: move the MARC21 rules for copying indicators
+                    // out of here
+                    if (headingField.tag == '130' && $scope.bibField.tag == '130') {
+                        $scope.bibField.ind1 = headingField.ind2;
+                    } else {
+                        $scope.bibField.ind1 = headingField.ind1;
+                    }
+                    // deal with 4xx and 5xx
+                    var authFallbackTag = '1' + headingField.tag.substr(1, 2);
+                    var _valid_auth_sfs = (headingField.tag in $scope._controlled_auth_sf_list) ?
+                                          $scope._controlled_auth_sf_list[headingField.tag] :
+                                          (authFallbackTag in $scope._controlled_auth_sf_list) ?
+                                          $scope._controlled_auth_sf_list[authFallbackTag] :
+                                          [];
+                    // save the $0 for later use
+                    var sfZero = '';
+                    if (headingField.subfield('0')) {
+                        sfZero = headingField.subfield('0')[1];
+                    }
+                    // grab any bib subfields not under authority control
+                    // TODO do something about uncontrolled subdivisions
+                    var uncontrolledBibSf = [];
+                    angular.forEach($scope.bibField.subfields, function(sf) {
+                        if (!(sf[0] in $scope._controlled_sf_list) && (sf[0] != '0')) {
+                            uncontrolledBibSf.push([ sf[0], sf[1] ]);
+                        }
+                    });
+                    // grab the authority subfields
+                    var authoritySf = [];
+                    angular.forEach(headingField.subfields, function(sf) {
+                        if (sf[0] in _valid_auth_sfs) {
+                            authoritySf.push([ sf[0], sf[1] ]);
+                        }
+                    });
+                    $scope.bibField.subfields.length = 0;
+                    angular.forEach(authoritySf, function(sf) {
+                        $scope.bibField.addSubfields(sf[0], sf[1]);
+                    });
+                    angular.forEach(uncontrolledBibSf, function(sf) {
+                        $scope.bibField.addSubfields(sf[0], sf[1]);
+                    });
+                    if (sfZero) {
+                        $scope.bibField.addSubfields('0', sfZero);
+                    }
+                    $scope.bibField.subfields.forEach(function (sf) {
+                    if (sf[0] in $scope._controlled_sf_list) {
+                            // intentionally not selecting any subfields
+                            // after we've applied an authority heading
+                            sf.selected = false;
+                            sf.selectable = true;
+                        } else {
+                            sf.selectable = false;
+                        }
+                    });
+                    $scope.changed = true;
+                }
+
                 $scope.createAuthorityFromBib = function(spawn_editor) {
                     var source_f = $scope.summarizeField();
 
@@ -1195,6 +1285,127 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
                         }
                     });
                 }
+
+            }
+        ]
+    }
+})
+
+.directive("egMarcEditAuthorityBrowser", function () {
+    return {
+        restrict: 'E',
+        replace: true,
+        templateUrl: './cat/share/t_authority_browser',
+        scope : {
+            searchString : '=',
+            controlSet : '=',
+            axis : '=',
+            applyHeading : '&'
+        },
+        controller: ['$scope','$http',
+            function ($scope , $http) {
+
+                $scope.page = 0;
+                $scope.limit = 10;
+                $scope.main_headings = [];
+
+                function getHeadingString(headingField) {
+                    var heading = '';
+                    angular.forEach(headingField.subfields, function (sf) {
+                        if (['x', 'y', 'z'].indexOf(sf[0]) > -1) {
+                            heading += ' --';
+                        }
+                        if (heading) {
+                            heading += ' ';
+                        }
+                        heading += sf[1];
+                    });
+                    return heading;
+                }
+
+                $scope.doBrowse = function() {
+                    $scope.main_headings.length = 0;
+                    if ($scope.searchString.length == 0) return;
+                    var type = 'authority.'
+                    var url = '/opac/extras/browse/marcxml/'
+                            + 'authority.' + $scope.axis + '.refs'
+                            + '/1' // OU - currently unscoped
+                            + '/' + $scope.searchString
+                            + '/' + $scope.page
+                            + '/' + $scope.limit;
+                    $http({
+                        url : url,
+                        method : 'GET',
+                        transformResponse : function(data) {
+                            // use a bit of jQuery to deal with the XML
+                            var $xml = $( $.parseXML(data) );
+                            var marc = [];
+                            $xml.find('record').each(function() {
+                                var rec = new MARC21.Record();
+                                rec.fromXmlDocument($(this)[0].outerHTML);
+                                marc.push(rec);
+                            });
+                            return marc;
+                        }
+                    }).then(function(response) {
+                        angular.forEach(response.data, function(rec) {
+                            var authId = rec.subfield('901', 'c')[1];
+                            var auth_org = '';
+                            if (rec.field('003')) {
+                                auth_org = rec.field('003').data;
+                            }
+                            var headingField = rec.field('1..');
+                            var seeAlsos = rec.field('4..', true);
+                            var seeFroms = rec.field('5..', true);
+
+                            var main_heading = {
+                                authority_id : authId,
+                                heading : getHeadingString(headingField),
+                                seealso_headings : [ ],
+                                seefrom_headings : [ ],
+                            };
+
+                            var sfZero = '';
+                            if (auth_org) {
+                                sfZero = '(' + auth_org + ')';
+                            }
+                            sfZero += authId;
+                            headingField.addSubfields('0', sfZero);
+
+                            main_heading['headingField'] = headingField;
+                            angular.forEach(seeAlsos, function(headingField) {
+                                main_heading.seealso_headings.push({
+                                    heading : getHeadingString(headingField),
+                                    headingField : headingField
+                                });
+                            });
+                            angular.forEach(seeFroms, function(headingField) {
+                                main_heading.seefrom_headings.push({
+                                    heading : getHeadingString(headingField),
+                                    headingField : headingField
+                                });
+                            });
+                            $scope.main_headings.push(main_heading);
+                        });
+                    });
+                }
+
+                $scope.$watch('searchString',
+                    function(newVal, oldVal) {
+                        if (newVal !== oldVal) {
+                            $scope.doBrowse();
+                        }
+                    }
+                );
+                $scope.$watch('page',
+                    function(newVal, oldVal) {
+                        if (newVal !== oldVal) {
+                            $scope.doBrowse();
+                        }
+                    }
+                );
+
+                $scope.doBrowse();
             }
         ]
     }

commit 573dd9fca6e8a4249a2ed38ffea0a37ae70c7426
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Mon Aug 31 22:09:01 2015 +0000

    webstaff: start work on authority linker modal
    
    When a bib record is opened in the MARC editor, fields that
    can be put under authority control now have a link button at
    the end of the subfield list. When clicked, this opens
    a dialog box that, for now, allows:
    
    * creating a new authority record based on the bib heading
    * creating a new authority record and launching a secondary
      MARC editor.
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/share/t_authority_link_dialog.tt2 b/Open-ILS/src/templates/staff/cat/share/t_authority_link_dialog.tt2
new file mode 100644
index 0000000..1787168
--- /dev/null
+++ b/Open-ILS/src/templates/staff/cat/share/t_authority_link_dialog.tt2
@@ -0,0 +1,15 @@
+<div>
+  <div class="modal-header">
+    <button type="button" class="close"
+      ng-click="cancel()" aria-hidden="true">×</button>
+    <h4 class="modal-title">[% l('Manage authority record links') %]</h4>
+  </div>
+  <div class="modal-body">
+    <eg-marc-edit-authority-linker bib-field="bibField" control-set="controlSet" changed="args.changed" />
+  </div>
+  <div class="modal-footer">
+    <input type="submit" ng-click="ok(args)"
+        class="btn btn-primary" value="[% l('Save') %]"/>
+    <button class="btn btn-warning" ng-click="cancel()">[% l('Cancel') %]</button>
+  </div>
+</div>
diff --git a/Open-ILS/src/templates/staff/cat/share/t_authority_linker.tt2 b/Open-ILS/src/templates/staff/cat/share/t_authority_linker.tt2
new file mode 100644
index 0000000..ac15751
--- /dev/null
+++ b/Open-ILS/src/templates/staff/cat/share/t_authority_linker.tt2
@@ -0,0 +1,18 @@
+<div>
+    <div class="row form-inline" style="font-family: 'Lucida Console', Monaco, monospace;">
+        {{bibField.tag}} {{bibField.ind1}}{{bibField.ind2}} 
+        <div ng-repeat="sf in bibField.subfields">
+            <span class="marcsfcodedelimiter">‡{{sf.0}}</span> {{sf.1}}
+            <input type="checkbox" ng-model="sf.selected" ng-if="sf.selectable" />
+        </div>
+    </div>
+    <div class="row form-inline">
+        [% l('Create new authority from this field') %]
+        <button class="btn btn-primary button-sm" ng-click="createAuthorityFromBib()">
+             [% l('Immediately') %]
+        </button>
+        <button class="btn btn-primary button-sm" ng-click="createAuthorityFromBib(true)">
+             [% l('Create and edit') %]
+        </button>
+    </div>
+</div>
diff --git a/Open-ILS/src/templates/staff/cat/share/t_edit_new_authority.tt2 b/Open-ILS/src/templates/staff/cat/share/t_edit_new_authority.tt2
new file mode 100644
index 0000000..d779dd2
--- /dev/null
+++ b/Open-ILS/src/templates/staff/cat/share/t_edit_new_authority.tt2
@@ -0,0 +1,15 @@
+<div>
+  <div class="modal-header">
+    <button type="button" class="close"
+      ng-click="cancel()" aria-hidden="true">×</button>
+    <h4 class="modal-title">[% l('Edit New Authority') %]</h4>
+  </div>
+  <div class="modal-body">
+    <eg-marc-edit-record dirty-flag="dirty_flag" record-type="are" record-id="args.authority_id" marc-xml="marc_xml" />
+  </div>
+  <div class="modal-footer">
+    <input type="submit" ng-click="ok(args)"
+        class="btn btn-primary" value="[% l('Use this authority') %]"/>
+    <button class="btn btn-warning" ng-click="cancel()">[% l('Cancel') %]</button>
+  </div>
+</div>
diff --git a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
index c31adcc..61ad57f 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
@@ -13,8 +13,19 @@ angular.module('egCatalogApp', ['ui.bootstrap','ngRoute','egCoreMod','egGridMod'
     $locationProvider.html5Mode(true);
     $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|blob):/); // grid export
 
-    var resolver = {delay : 
-        ['egStartup', function(egStartup) {return egStartup.go()}]}
+    var resolver = {delay : ['egCore','egStartup', function(egCore,  egStartup) {
+        egCore.env.classLoaders.aous = function() {
+            return egCore.org.settings([
+                'cat.marc_control_number_identifier'
+            ]).then(function(settings) {
+                // local settings are cached within egOrg.  Caching them
+                // again in egEnv just simplifies the syntax for access.
+                egCore.env.aous = settings;
+            });
+        }
+        egCore.env.loadClasses.push('aous');
+        return egStartup.go()
+    }]};
 
     $routeProvider.when('/cat/catalog/index', {
         templateUrl: './cat/catalog/t_catalog',
diff --git a/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js b/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
index e977cef..2ed3339 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
@@ -364,9 +364,59 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
                     '<span><eg-marc-edit-ind field="field" ind="field.ind1" on-keydown="onKeydown" ind-number="1"/></span>'+
                     '<span><eg-marc-edit-ind field="field" ind="field.ind2" on-keydown="onKeydown" ind-number="2"/></span>'+
                     '<span><eg-marc-edit-subfield ng-repeat="subfield in field.subfields" subfield="subfield" field="field" on-keydown="onKeydown"/></span>'+
+                    // FIXME: template should probably be moved to file to improve
+                    // translatibility
+                    '<button class="btn btn-info btn-xs" '+
+                    'aria-label="Manage authority record links" '+
+                    'ng-show="isAuthorityControlled(field)"'+
+                    'ng-click="spawnAuthorityLinker()"'+
+                    '>'+
+                    '<span class="glyphicon glyphicon-link"></span>'+
+                    '</button>'+
                   '</div>',
         scope: { field: "=", onKeydown: '=' },
-        replace: true
+        replace: true,
+        controller : ['$scope','$modal',
+            function ( $scope,  $modal ) {
+                $scope.isAuthorityControlled = function () {
+                    return ($scope.$parent.$parent.record_type == 'bre') &&
+                           $scope.$parent.$parent.controlSet.bibFieldByTag($scope.field.tag);
+                }
+                $scope.spawnAuthorityLinker = function() {
+                    // intentionally making a clone in case
+                    // user decides to abandon the linking
+                    var fieldCopy = new MARC21.Field({
+                        tag       : $scope.field.tag,
+                        ind1      : $scope.field.ind1,
+                        ind2      : $scope.field.ind2
+                    });
+                    angular.forEach($scope.field.subfields, function(sf) {
+                        fieldCopy.subfields.push(sf.slice(0));
+                    });
+                    var cs = $scope.$parent.$parent.controlSet;
+                    var args = { changed : false };
+                    $modal.open({
+                        templateUrl: './cat/share/t_authority_link_dialog',
+                        size: 'md',
+                        controller: ['$scope', '$modalInstance', function($scope, $modalInstance) {
+                            $scope.controlSet = cs;
+                            $scope.bibField = fieldCopy;
+                            $scope.focusMe = true;
+                            $scope.args = args;
+                            $scope.ok = function(args) { $modalInstance.close(args) };
+                            $scope.cancel = function () { $modalInstance.dismiss() };
+                        }]
+                    }).result.then(function (args) {
+                        if (args.changed) {
+                            $scope.field.subfields.length = 0;
+                            angular.forEach(fieldCopy.subfields, function(sf) {
+                                $scope.field.addSubfields(sf[0], sf[1]);
+                            });
+                        }
+                    });
+                }
+            }
+        ]
     }
 })
 
@@ -819,9 +869,15 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
                                 deferred.resolve(rec);
                             });
                         } else {
-                            var bre = new egCore.idl.bre();
-                            bre.marc($scope.marcXml);
-                            deferred.resolve(bre);
+                            if ($scope.recordType == 'bre') {
+                                var bre = new egCore.idl.bre();
+                                bre.marc($scope.marcXml);
+                                deferred.resolve(bre);
+                            } else if ($scope.recordType == 'are') {
+                                var are = new egCore.idl.are();
+                                are.marc($scope.marcXml);
+                                deferred.resolve(are);
+                            }
                             $scope.brandNewRecord = true;
                         }
                         return deferred.promise;
@@ -1042,4 +1098,106 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
     }
 }])
 
+.directive("egMarcEditAuthorityLinker", function () {
+    return {
+        restrict: 'E',
+        replace: true,
+        templateUrl: './cat/share/t_authority_linker',
+        scope : {
+            bibField : '=',
+            controlSet : '=',
+            changed : '='
+        },
+        controller: ['$scope','$modal','egCore','egAuth',
+            function ($scope , $modal,  egCore,  egAuth) {
+
+                var cni = egCore.env.aous['cat.marc_control_number_identifier'] ||
+                  'Set cat.marc_control_number_identifier in Library Settings';
+                
+                $scope._controlled_sf_list = {};
+                var found_acs = [];
+                angular.forEach($scope.controlSet.controlSetList(), function(acs_id) {
+                    if ($scope.controlSet.controlSet(acs_id).control_map[$scope.bibField.tag])
+                        found_acs.push(acs_id);
+                });
+                if (found_acs.length) {
+                     angular.forEach(
+                        $scope.controlSet.controlSet(found_acs[0]).control_map[$scope.bibField.tag],                         function(sf) {
+                            $scope._controlled_sf_list[ sf[$scope.bibField.tag] ] = 1;
+                        }
+                    )
+                }
+
+                $scope.bibField.subfields.forEach(function (sf) {
+                    if (sf[0] in $scope._controlled_sf_list) {
+                        sf.selected = true;
+                        sf.selectable = true;
+                    } else {
+                        sf.selectable = false;
+                    }
+                });
+                $scope.summarizeField = function() {
+                    var source_f = {
+                        'tag': $scope.bibField.tag,
+                        'ind1': $scope.bibField.ind1,
+                        'ind2': $scope.bibField.ind2,
+                        'subfields': []
+                    };
+                    $scope.bibField.subfields.forEach(function(sf) {
+                        if (sf.selected) {
+                            source_f.subfields.push([ sf[0], sf[1] ]);
+                        }
+                    });
+                    return source_f;
+                }
+
+                $scope.updateSubfieldZero = function(value) {
+                    $scope.changed = true;
+                    $scope.bibField.deleteSubfield({ code : ['0'] });
+                    $scope.bibField.subfields.push([
+                        '0', '(' + cni + ')' + value
+                    ]);
+                };
+
+                $scope.createAuthorityFromBib = function(spawn_editor) {
+                    var source_f = $scope.summarizeField();
+
+                    var args = { authority_id : 0 };
+                    var method = (spawn_editor) ?
+                        'open-ils.cat.authority.record.create_from_bib.readonly' :
+                        'open-ils.cat.authority.record.create_from_bib';
+                    egCore.net.request(
+                        'open-ils.cat',
+                        method,
+                        source_f,
+                        cni,
+                        egAuth.token()
+                    ).then(function(newAuthority) {
+                        if (spawn_editor) {
+                            $modal.open({
+                                templateUrl: './cat/share/t_edit_new_authority',
+                                size: 'lg',
+                                controller:
+                                    ['$scope', '$modalInstance', function($scope, $modalInstance) {
+                                    $scope.focusMe = true;
+                                    $scope.args = args;
+                                    $scope.dirty_flag = false;
+                                    $scope.marc_xml = newAuthority,
+                                    $scope.ok = function(args) { $modalInstance.close(args) }
+                                    $scope.cancel = function () { $modalInstance.dismiss() }
+                                }]
+                            }).result.then(function (args) {
+                                if (!args || !args.authority_id) return;
+                                $scope.updateSubfieldZero(args.authority_id);
+                            });
+                        } else {
+                            $scope.updateSubfieldZero(newAuthority.id());
+                        }
+                    });
+                }
+            }
+        ]
+    }
+})
+
 ;

commit 896d2f5aa4dc21acf70257f277273adb7ebb8d7a
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Mon Aug 31 22:06:23 2015 +0000

    webstaff: fix bug in MARC21.Field.deleteSubfield()
    
    Bug had the effect of ensuring that... subfields would
    not be deleted.
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/marcrecord.js b/Open-ILS/web/js/ui/default/staff/marcrecord.js
index 7c3f525..2f03706 100644
--- a/Open-ILS/web/js/ui/default/staff/marcrecord.js
+++ b/Open-ILS/web/js/ui/default/staff/marcrecord.js
@@ -613,7 +613,7 @@ var MARC21 = {
 
             for (var i = 0; i < args.code.length; i++) {
                 var sub_pos = {};
-                for (var j = 0; j < me.subfields; j++) {
+                for (var j = 0; j < me.subfields.length; j++) {
                     if (me.subfields[j][0] == args.code[i]) {
 
                         if (!sub_pos[args.code[i]]) sub_pos[args.code[j]] = 0;

commit 1213ce7265dba43620ef31997e470ee1e444cf44
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Mon Aug 31 17:33:30 2015 +0000

    webstaff: improve initialization of egTable.authorityControlSet()
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/cat/services/tagtable.js b/Open-ILS/web/js/ui/default/staff/cat/services/tagtable.js
index b40597e..6b40c9c 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/services/tagtable.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/services/tagtable.js
@@ -350,6 +350,11 @@ function($q,   egCore,   egAuth) {
             }).then(function() {
                 service.authority_control_set._remote_loaded = true;
                 parent._parse();
+                if (kwargs.controlSet) {
+                    parent.controlSetId( kwargs.controlSet );
+                } else {
+                    parent.controlSetId( parent.controlSetList().sort(function(a,b){return (a - b)}) );
+                }
             });
         }
 
@@ -487,13 +492,7 @@ function($q,   egCore,   egAuth) {
                 this.bibToAuthorities(field)
             );
         }
-    
-        if (kwargs.controlSet) {
-            this.controlSetId( kwargs.controlSet );
-        } else {
-            this.controlSetId( this.controlSetList().sort(function(a,b){return (a - b)}) );
-        }
-    
+
     }
 
     return service;

commit a80485baefde89e1c9dd8c3fe712e8c3a11f1f05
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Mon Aug 31 17:27:36 2015 +0000

    webstaff: port authority control set from MARC21 to egTagTable
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js b/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
index 1345415..e977cef 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
@@ -468,6 +468,7 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
                 $scope.save_stack_depth = 0;
                 $scope.controlfields = [];
                 $scope.datafields = [];
+                $scope.controlSet = new egTagTable.authorityControlSet();
 
                 egTagTable.loadTagTable({ marcRecordType : $scope.record_type });
 
diff --git a/Open-ILS/web/js/ui/default/staff/cat/services/tagtable.js b/Open-ILS/web/js/ui/default/staff/cat/services/tagtable.js
index f726f2e..b40597e 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/services/tagtable.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/services/tagtable.js
@@ -13,7 +13,10 @@ function($q,   egCore,   egAuth) {
         },
         fields : { },
         ff_pos_map : { },
-        ff_value_map : { }
+        ff_value_map : { },
+        authority_control_set : {
+            _controlsets : [ ]
+        }
     };
 
     // allow 'bre' and 'biblio' to be synonyms, etc.
@@ -180,5 +183,318 @@ function($q,   egCore,   egAuth) {
         return list;
     }
 
+    service.authorityControlSet = function (kwargs) {
+    
+        kwargs = kwargs || {};
+
+        this._fetch_class = function(hint, cache_key) {
+            return egCore.pcrud.retrieveAll(hint, {}, {atomic : true}).then(
+                function(list) {
+                    egCore.env.absorbList(list, hint);
+                    service.authority_control_set[cache_key] = list;
+                }
+            );
+        };
+
+        this._fetch = function(cmap) {
+            var deferred = $q.defer();
+            var promises = [];
+            for (var hint in cmap) {
+                promises.push(this._fetch_class(hint, cmap[hint]));
+            }
+            $q.all(promises).then(function() {
+                deferred.resolve();
+            });
+            return deferred.promise;
+        };
+
+        this._parse = function() {
+            service.authority_control_set._browse_axis_by_code = {};
+            service.authority_control_set._browse_axis_list.forEach(function (ba) {
+                ba.maps(
+                    service.authority_control_set._browse_field_map_list.filter(
+                        function (m) { return m.axis() == ba.code() }
+                    )
+                );
+                service.authority_control_set._browse_axis_by_code[ba.code()] = ba;
+            });
+    
+            // loop over each acs
+            service.authority_control_set._control_set_list.forEach(function (cs) {
+                service.authority_control_set._controlsets[''+cs.id()] = {
+                    id : cs.id(),
+                    name : cs.name(),
+                    description : cs.description(),
+                    authority_tag_map : {},
+                    control_map : {},
+                    bib_fields : [],
+                    raw : cs
+                };
+    
+                // grab the authority fields
+                var acsaf_list = service.authority_control_set._authority_field_list.filter(
+                    function (af) { return af.control_set() == cs.id() }
+                );
+    
+                var at_list = service.authority_control_set._thesaurus_list.filter(
+                    function (at) { return at.control_set() == cs.id() }
+                );
+    
+                service.authority_control_set._controlsets[''+cs.id()].raw.authority_fields( acsaf_list );
+                service.authority_control_set._controlsets[''+cs.id()].raw.thesauri( at_list );
+    
+                // and loop over each
+                acsaf_list.forEach(function (csaf) {
+                    csaf.axis_maps([]);
+    
+                    // link the main entry if we're subordinate
+                    if (csaf.main_entry()) {
+                        csaf.main_entry(
+                            acsaf_list.filter(function (x) {
+                                return x.id() == csaf.main_entry();
+                            })[0]
+                        );
+                    }
+    
+                    // link the sub entries if we're main
+                    csaf.sub_entries(
+                        acsaf_list.filter(function (x) {
+                            return x.main_entry() == csaf.id();
+                        })
+                    );
+    
+                    // now, bib fields
+                    var acsbf_list = service.authority_control_set._bib_field_list.filter(
+                        function (b) { return b.authority_field() == csaf.id() }
+                    );
+                    csaf.bib_fields( acsbf_list );
+    
+                    service.authority_control_set._controlsets[''+cs.id()].bib_fields = [].concat(
+                        service.authority_control_set._controlsets[''+cs.id()].bib_fields,
+                        acsbf_list
+                    );
+    
+                    acsbf_list.forEach(function (csbf) {
+                        // link the authority field to the bib field
+                        if (csbf.authority_field()) {
+                            csbf.authority_field(
+                                acsaf_list.filter(function (x) {
+                                    return x.id() == csbf.authority_field();
+                                })[0]
+                            );
+                        }
+    
+                    });
+    
+                    service.authority_control_set._browse_axis_list.forEach(
+                        function (ba) {
+                            ba.maps().filter(
+                                function (m) { return m.field() == csaf.id() }
+                            ).forEach(
+                                function (fm) { fm.field( csaf ); csaf.axis_maps().push( fm ) } // and set the field
+                            )
+                        }
+                    );
+    
+                });
+    
+                // build the authority_tag_map
+                service.authority_control_set._controlsets[''+cs.id()].bib_fields.forEach(function (bf) {
+    
+                    if (!service.authority_control_set._controlsets[''+cs.id()].control_map[bf.tag()])
+                        service.authority_control_set._controlsets[''+cs.id()].control_map[bf.tag()] = {};
+    
+                    bf.authority_field().sf_list().split('').forEach(function (sf_code) {
+    
+                        if (!service.authority_control_set._controlsets[''+cs.id()].control_map[bf.tag()][sf_code])
+                            service.authority_control_set._controlsets[''+cs.id()].control_map[bf.tag()][sf_code] = {};
+    
+                        service.authority_control_set._controlsets[''+cs.id()].control_map[bf.tag()][sf_code][bf.authority_field().tag()] = sf_code;
+                    });
+                });
+    
+            });
+    
+            if (this.controlSetList().length > 0)
+                delete service.authority_control_set._controlsets['-1'];
+    
+        }
+    
+        this.controlSetId = function (x) {
+            if (x) this._controlset = ''+x;
+            return this._controlset;
+        }
+
+        this.controlSetList = function () {
+            var l = [];
+            for (var i in service.authority_control_set._controlsets) {
+                l.push(i);
+            }
+            return l;
+        }
+    
+    
+        if (!service.authority_control_set._remote_loaded) {
+    
+            // TODO -- push the raw tree into the oils cache for later reuse
+    
+            // fetch everything up front...
+            var parent = this;
+            this._fetch({
+                "acs": "_control_set_list",
+                "at": "_thesaurus_list",
+                "acsaf": "_authority_field_list",
+                "acsbf": "_bib_field_list",
+                "aba": "_browse_axis_list",
+                "abaafm": "_browse_field_map_list"
+            }).then(function() {
+                service.authority_control_set._remote_loaded = true;
+                parent._parse();
+            });
+        }
+
+        this.controlSet = function (x) {
+            return service.authority_control_set._controlsets[''+this.controlSetId(x)];
+        }
+    
+        this.controlSetByThesaurusCode = function (x) {
+            var thes = service.authority_control_set._thesaurus_list.filter(
+                function (at) { return at.code() == x }
+            )[0];
+    
+            return this.controlSet(thes.control_set());
+        }
+    
+        this.browseAxisByCode = function(code) {
+            return service.authority_control_set._browse_axis_by_code[code];
+        }
+    
+        this.bibFieldByTag = function (x) {
+            var me = this;
+            return me.controlSet().bib_fields.filter(
+                function (bf) { if (bf.tag() == x) return true }
+            )[0];
+        }
+    
+        this.bibFields = function (x) {
+            return this.controlSet(x).bib_fields;
+        }
+    
+        this.bibFieldBrowseAxes = function (t) {
+            var blist = [];
+            for (var bcode in service.authority_control_set._browse_axis_by_code) {
+                service.authority_control_set._browse_axis_by_code[bcode].maps().forEach(
+                    function (m) {
+                        if (m.field().bib_fields().filter(
+                                function (b) { return b.tag() == t }
+                            ).length > 0
+                        ) blist.push(bcode);
+                    }
+                );
+            }
+            return blist;
+        }
+    
+        this.authorityFields = function (x) {
+            return this.controlSet(x).raw.authority_fields();
+        }
+    
+        this.thesauri = function (x) {
+            return this.controlSet(x).raw.thesauri();
+        }
+    
+        this.findControlSetsForTag = function (tag) {
+            var me = this;
+            var old_acs = this.controlSetId();
+            var acs_list = me.controlSetList().filter(
+                function(acs_id) { return (me.controlSet(acs_id).control_map[tag]) }
+            );
+            this.controlSetId(old_acs);
+            return acs_list;
+        }
+    
+        this.findControlSetsForAuthorityTag = function (tag) {
+            var me = this;
+            var old_acs = this.controlSetId();
+    
+            var acs_list = me.controlSetList().filter(
+                function(acs_id) {
+                    var a = me.controlSet(acs_id);
+                    for (var btag in a.control_map) {
+                        for (var sf in a.control_map[btag]) {
+                            if (a.control_map[btag][sf][tag]) return true;
+                        }
+                    }
+                    return false;
+                }
+            );
+            this.controlSetId(old_acs);
+            return acs_list;
+        }
+    
+        this.bibToAuthority = function (field) {
+            var b_field = this.bibFieldByTag(field.tag);
+    
+            if (b_field) { // construct an marc authority record
+                var af = b_field.authority_field();
+    
+                var sflist = [];                
+                for (var i = 0; i < field.subfields.length; i++) {
+                    if (af.sf_list().indexOf(field.subfields[i][0]) > -1) {
+                        sflist.push(field.subfields[i]);
+                    }
+                }
+    
+                var m = new MARC21.Record ({rtype:'AUT'});
+                m.appendFields(
+                    new MARC21.Field ({
+                        tag : af.tag(),
+                        ind1: field.ind1,
+                        ind2: field.ind2,
+                        subfields: sflist
+                    })
+                );
+    
+                return m.toXmlString();
+            }
+    
+            return null;
+        }
+    
+        this.bibToAuthorities = function (field) {
+            var auth_list = [];
+            var me = this;
+    
+            var old_acs = this.controlSetId();
+            me.controlSetList().forEach(
+                function (acs_id) {
+                    var acs = me.controlSet(acs_id);
+                    var x = me.bibToAuthority(field);
+                    if (x) { var foo = {}; foo[acs_id] = x; auth_list.push(foo); }
+                }
+            );
+            this.controlSetId(old_acs);
+    
+            return auth_list;
+        }
+    
+        // This should not be used in an angular world.  Instead, the call
+        // to open-ils.search.authority.simple_heading.from_xml.batch.atomic should
+        // be performed by the code that wants to find matching authorities.
+        this.findMatchingAuthorities = function (field) {
+            return fieldmapper.standardRequest(
+                [ 'open-ils.search', 'open-ils.search.authority.simple_heading.from_xml.batch.atomic' ],
+                this.bibToAuthorities(field)
+            );
+        }
+    
+        if (kwargs.controlSet) {
+            this.controlSetId( kwargs.controlSet );
+        } else {
+            this.controlSetId( this.controlSetList().sort(function(a,b){return (a - b)}) );
+        }
+    
+    }
+
     return service;
 }]);

commit 53fa1c6856e16e0b8567cdd6056031faa9762fc1
Author: Mike Rylander <mrylander at gmail.com>
Date:   Mon Aug 31 15:50:04 2015 -0400

    webstaff: Show the column picker for working and completed grids
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2 b/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2
index 9c43e98..b067b19 100644
--- a/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2
@@ -82,7 +82,7 @@
                         <eg-grid
                           id-field="id"
                           idl-class="acp"
-                          features="startSelected,-pagination,-actions,-picker,-index"
+                          features="startSelected,-pagination,-actions,-index"
                           items-provider="workingGridDataProvider"
                           grid-controls="workingGridControls"
                           on-select="handleItemSelect"
@@ -113,7 +113,7 @@
                <eg-grid
                  id-field="id"
                  idl-class="acp"
-                 features="-pagination,-actions,-picker,-index"
+                 features="-pagination,-actions,-index"
                  items-provider="completedGridDataProvider"
                  grid-controls="completedGridControls"
                  persist-key="cat.volcopy.copies.complete">

commit aee3eb930e3bda9336a0d36342cee43e7e36befe
Author: Mike Rylander <mrylander at gmail.com>
Date:   Mon Aug 31 15:48:51 2015 -0400

    webstaff: Remove the page-level controller
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/volcopy/index.tt2 b/Open-ILS/src/templates/staff/cat/volcopy/index.tt2
index d3f3ccf..132e21a 100644
--- a/Open-ILS/src/templates/staff/cat/volcopy/index.tt2
+++ b/Open-ILS/src/templates/staff/cat/volcopy/index.tt2
@@ -2,7 +2,6 @@
   WRAPPER "staff/base.tt2";
   ctx.page_title = l("Volume/Copy Editor"); 
   ctx.page_app = "egVolCopy";
-  ctx.page_ctrl = "EditCtrl";
 %]
 
 [% BLOCK APP_JS %]

commit f8b9fbc203732300ba7bef3da3bf1eaac3f1cafd
Author: Bill Erickson <berickxx at gmail.com>
Date:   Wed Aug 19 22:22:00 2015 -0400

    LP#1452950 reduce console errors on obscure dob check
    
    Avoid referencing egCore.env.* before env values have had time to arrive
    from the server.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/circ/patron/app.js b/Open-ILS/web/js/ui/default/staff/circ/patron/app.js
index 1b80839..d5fb95d 100644
--- a/Open-ILS/web/js/ui/default/staff/circ/patron/app.js
+++ b/Open-ILS/web/js/ui/default/staff/circ/patron/app.js
@@ -620,8 +620,14 @@ function($scope,  $q,  $location , $filter,  egCore,  egUser,  patronSvc) {
         return !egCore.env.aous['circ.obscure_dob'];
     }
         
-    $scope.obscure_dob = function() { return egCore.env.aous['circ.obscure_dob']; }
-    $scope.now_show_dob = function() { return egCore.env.aous['circ.obscure_dob'] ? $scope.show_dob() : true; }
+    $scope.obscure_dob = function() { 
+        return egCore.env.aous && egCore.env.aous['circ.obscure_dob'];
+    }
+    $scope.now_show_dob = function() { 
+        return egCore.env.aous && egCore.env.aous['circ.obscure_dob'] ?
+            $scope.show_dob() : true; 
+    }
+
     $scope.patron = function() { return patronSvc.current }
     $scope.patron_stats = function() { return patronSvc.patron_stats }
     $scope.summary_stat_cats = function() { return patronSvc.summary_stat_cats }

commit 7c2b5a004f2490e41a1142e460d8fa81929f78a7
Author: Mike Rylander <mrylander at gmail.com>
Date:   Mon Aug 31 13:22:10 2015 -0400

    webstaff: Unbreak grid feature detection
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/services/grid.js b/Open-ILS/web/js/ui/default/staff/services/grid.js
index 5e399b0..02e462c 100644
--- a/Open-ILS/web/js/ui/default/staff/services/grid.js
+++ b/Open-ILS/web/js/ui/default/staff/services/grid.js
@@ -126,14 +126,18 @@ angular.module('egGridMod',
                 $scope.actionGroups = [{actions:[]}]; // Grouped actions for selected items
                 $scope.menuItems = []; // global actions
 
-                $scope.showIndex = ($scope.features.indexOf('-index') == -1);
+                var features = ($scope.features) ? 
+                    $scope.features.split(',') : [];
+                delete $scope.features;
+
+                $scope.showIndex = (features.indexOf('-index') == -1);
 
-                $scope.startSelected = $scope.selectAll = ($scope.features.indexOf('startSelected') > -1);
-                $scope.showActions = ($scope.features.indexOf('-actions') == -1);
-                $scope.showPagination = ($scope.features.indexOf('-pagination') == -1);
-                $scope.showPicker = ($scope.features.indexOf('-picker') == -1);
+                $scope.startSelected = $scope.selectAll = (features.indexOf('startSelected') > -1);
+                $scope.showActions = (features.indexOf('-actions') == -1);
+                $scope.showPagination = (features.indexOf('-pagination') == -1);
+                $scope.showPicker = (features.indexOf('-picker') == -1);
 
-                $scope.showMenu = ($scope.features.indexOf('-menu') == -1);
+                $scope.showMenu = (features.indexOf('-menu') == -1);
 
                 // remove some unneeded values from the scope to reduce bloat
 
@@ -161,10 +165,6 @@ angular.module('egGridMod',
 
                 grid.dataProvider = $scope.itemsProvider;
 
-                var features = ($scope.features) ? 
-                    $scope.features.split(',') : [];
-                delete $scope.features;
-
                 if (!grid.indexField && grid.idlClass)
                     grid.indexField = egCore.idl.classes[grid.idlClass].pkey;
 

commit cee5310eb751aed14670d68e8a524480bc94cb52
Author: Mike Rylander <mrylander at gmail.com>
Date:   Fri Aug 28 17:46:06 2015 -0400

    webstaff: Stat cat editing, templating, and defaults
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2 b/Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2
index e8fe8bc..b38db52 100644
--- a/Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2
+++ b/Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2
@@ -1,48 +1,48 @@
 <div class="container-fluid">
-<div class="row bg-info">
-    <div class="col-md-1">
-        <h5>[% l('Template') %]</h5>
-    </div>
-    <div class="col-md-2">
-        <eg-basic-combo-box list="template_name_list" selected="template_name"></eg-basic-combo-box>
-    </div>
-    <div class="col-md-1">
-        <button class="btn btn-default " ng-click="applyTemplate(template_name)" type="button">[% l('Apply') %]</button>
-    </div>
-    <div class="col-md-6">
-        <div class="row" ng-show="template_controls">
-            <div class="col-md-4">
-                <div class="btn-group">
-                    <label class="btn btn-default" ng-click="saveTemplate(template_name)">[% l('Save') %]</label>
-                    <label class="btn btn-default" ng-click="deleteTemplate(template_name)">[% l('Delete') %]</label>
+    <div class="row bg-info">
+        <div class="col-md-1">
+            <h5>[% l('Template') %]</h5>
+        </div>
+        <div class="col-md-2">
+            <eg-basic-combo-box list="template_name_list" selected="template_name"></eg-basic-combo-box>
+        </div>
+        <div class="col-md-1">
+            <button class="btn btn-default " ng-click="applyTemplate(template_name)" type="button">[% l('Apply') %]</button>
+        </div>
+        <div class="col-md-6">
+            <div class="row" ng-show="template_controls">
+                <div class="col-md-4">
+                    <div class="btn-group">
+                        <label class="btn btn-default" ng-click="saveTemplate(template_name)">[% l('Save') %]</label>
+                        <label class="btn btn-default" ng-click="deleteTemplate(template_name)">[% l('Delete') %]</label>
+                    </div>
                 </div>
-            </div>
-            <div class="col-md-8">
-                <div class="btn-group pull-right">
-                    <label class="btn btn-default" ng-click="importTemplates()">[% l('Import') %]</label>
-                    <label class="btn btn-default" ng-click="exportTemplates()">[% l('Export') %]</label>
+                <div class="col-md-8">
+                    <div class="btn-group pull-right">
+                        <label class="btn btn-default" ng-click="importTemplates()">[% l('Import') %]</label>
+                        <label class="btn btn-default" ng-click="exportTemplates()">[% l('Export') %]</label>
+                    </div>
                 </div>
             </div>
         </div>
+        <div class="col-md-2">
+            <button class="btn btn-default pull-right" ng-click="clearWorking()" type="button">Clear</button>
+        </div>
     </div>
-    <div class="col-md-2">
-        <button class="btn btn-default pull-right" ng-click="clearWorking()" type="button">Clear</button>
-    </div>
-</div>
 
-<div class="row pad-vert"></div>
+    <div class="row pad-vert"></div>
 
-<div class="row bg-info">
-    <div class="col-md-4">
-        <b>[% l('Circulate?') %]</b>
-    </div>
-    <div class="col-md-4">
-        <b>[% l('Status') %]</b>
-    </div>
-    <div class="col-md-4">
-        <b>[% l('Statistical Catagories') %]</b>
+    <div class="row bg-info">
+        <div class="col-md-4">
+            <b>[% l('Circulate?') %]</b>
+        </div>
+        <div class="col-md-4">
+            <b>[% l('Status') %]</b>
+        </div>
+        <div class="col-md-4">
+            <b>[% l('Statistical Catagories') %]</b>
+        </div>
     </div>
-</div>
 
     <div class="row">
         <div class="col-md-8">
@@ -66,8 +66,8 @@
                 <div class="col-md-6" ng-class="{'bg-success': working.status !== undefined}">
                     <select class="form-control"
                         ng-disabled="!defaults.attributes.status" ng-model="working.status"
-                        ng-options="s.id() as s.name() for s in status_list"
-                    ></select>
+                        ng-options="s.id() as s.name() for s in status_list">
+                    </select>
                 </div>
             </div>
 
@@ -158,7 +158,7 @@
             </div>
 
             <div class="row">
-                <div class="col-md-6" ng-class="{'bg-success': working.circ_modifier !== undefined}">
+                <div class="nullable col-md-6" ng-class="{'bg-success': working.circ_modifier !== undefined}">
                     <select class="form-control"
                         ng-disabled="!defaults.attributes.circ_modifier" ng-model="working.circ_modifier"
                         ng-options="m.code() as m.name() for m in circ_modifier_list"
@@ -207,11 +207,12 @@
             </div>
 
             <div class="row">
-                <div class="col-md-6" ng-class="{'bg-success': working.circ_as_type !== undefined}">
+                <div class="nullable col-md-6" ng-class="{'bg-success': working.circ_as_type !== undefined}">
                     <select class="form-control"
                         ng-disabled="!defaults.attributes.circ_as_type" ng-model="working.circ_as_type"
-                        ng-options="t.code() as t.value() for t in circ_type_list"
-                    ></select>
+                        ng-options="t.code() as t.value() for t in circ_type_list">
+                      <option value="">[% l('<NONE>') %]</option>
+                    </select>
                 </div>
                 <div class="col-md-6" ng-class="{'bg-success': working.deposit !== undefined}">
                     <div class="row">
@@ -318,22 +319,41 @@
                 </div>
             </div>
         </div>
-        <div class="col=md-4">
-            statcats<br/>
-            statcats<br/>
-            statcats<br/>
-            statcats<br/>
-            statcats<br/>
-            statcats<br/>
-            statcats<br/>
-            statcats<br/>
-            statcats<br/>
-            statcats<br/>
-            statcats<br/>
-            statcats<br/>
-            statcats<br/>
-            statcats<br/>
+
+        <div class="col-md-4">
+            <div class="row">
+                <div class="col-xs-12">
+                    <select class="form-control" ng-disabled="!defaults.statcats"
+                        ng-model="working.statcat_filter"
+                        ng-options="o.id() as o.shortname() for o in statcat_filter_list">
+                      <option value="">[% l('Filter by Library') %]</option>
+                    </select>
+                </div>
+            </div>
+
+            <div class="row pad-vert"></div>
+
+            <div class="row" ng-repeat="sc in statcats">
+                <div class="col-xs-12">
+                    <div class="row bg-info">
+                        <div class="col-xs-12">
+                            <span>{{ sc.owner().name() }} : {{ sc.name() }}</span>
+                        </div>
+                    </div>
+                    <div class="row">
+                        <div class="nullable col-xs-12" ng-class="{'bg-success': working.statcats[sc.id()] !== undefined}">
+                            <select class="form-control" ng-disabled="!defaults.statcats"
+                                ng-change="statcatUpdate(sc.id())"
+                                ng-model="working.statcats[sc.id()]"
+                                ng-options="e.id() as e.value() for e in sc.entries()">
+                                <option value="">[% l('<NONE>') %]</option>
+                            </select>
+                        </div>
+                    </div>
+                </div>
+            </div>
         </div>
+
     </div>
 </div>
 </div>
diff --git a/Open-ILS/src/templates/staff/cat/volcopy/t_defaults.tt2 b/Open-ILS/src/templates/staff/cat/volcopy/t_defaults.tt2
index 9f74e13..44c28d8 100644
--- a/Open-ILS/src/templates/staff/cat/volcopy/t_defaults.tt2
+++ b/Open-ILS/src/templates/staff/cat/volcopy/t_defaults.tt2
@@ -241,7 +241,7 @@
                 </div>
                 <div class="col-xs-6">
                     <label>
-                        <eg-org-selector selected="defaults.statcats.org_filter" noDefault label="[% l('Default Filter Library') %]" disableTest="cant_have_vols"></eg-org-selector>
+                        <eg-org-selector selected="defaults.statcat_filter" noDefault label="[% l('Default Filter Library') %]" disableTest="cant_have_vols"></eg-org-selector>
                     </label>
                 </div>
             </div>
@@ -254,6 +254,10 @@
                     </label>
                 </div>
                 <div class="col-xs-6">
+                    <label>
+                        <input type="checkbox" ng-change="saveDefaults()" ng-model="defaults.statcats"/>
+                        [% l('Edit Statistical Data') %]
+                    </label>
                 </div>
             </div>
 
diff --git a/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2 b/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2
index c663610..9c43e98 100644
--- a/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2
@@ -85,6 +85,8 @@
                           features="startSelected,-pagination,-actions,-picker,-index"
                           items-provider="workingGridDataProvider"
                           grid-controls="workingGridControls"
+                          on-select="handleItemSelect"
+                          after-select="afterItemSelect"
                           persist-key="cat.volcopy.copies">
         
                           <eg-grid-menu-item handler="workingToComplete"
diff --git a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
index 27498fc..68aab84 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
@@ -56,6 +56,18 @@ function(egCore , $q) {
 
     };
 
+    service.get_statcats = function(orgs) {
+        return egCore.pcrud.search('asc',
+            {owner : orgs},
+            { flesh : 1,
+              flesh_fields : {
+                asc : ['owner','entries']
+              }
+            },
+            { atomic : true }
+        );
+    };
+
     service.get_locations = function(orgs) {
         return egCore.pcrud.search('acpl',
             {owning_lib : orgs},
@@ -141,7 +153,7 @@ function(egCore , $q) {
     service.flesh = {   
         flesh : 3, 
         flesh_fields : {
-            acp : ['call_number','parts'],
+            acp : ['call_number','parts','stat_cat_entries'],
             acn : ['label_class','prefix','suffix']
         }
     }
@@ -483,6 +495,7 @@ function(egCore , $q) {
 function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , egGridDataProvider , itemSvc) {
 
     $scope.defaults = { // If defaults are not set at all, allow everything
+        statcats : true,
         attributes : {
             status : true,
             loan_duration : true,
@@ -517,6 +530,7 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
                 $scope.batch.classification = $scope.defaults.classification;
                 $scope.batch.prefix = $scope.defaults.prefix;
                 $scope.batch.suffix = $scope.defaults.suffix;
+                $scope.working.statcat_filter = $scope.defaults.statcat_filter;
                 if ($scope.defaults.always_vols) $scope.show_vols = true;
             }
         });
@@ -559,7 +573,7 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
     }
 
     createSimpleUpdateWatcher = function (field) {
-        $scope.$watch('working.' + field, function () {
+        return $scope.$watch('working.' + field, function () {
             var newval = $scope.working[field];
 
             if (typeof newval != 'undefined') {
@@ -568,7 +582,7 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
                     else if (newval.code) newval = newval.code();
                 }
 
-                if (newval == "") {
+                if (""+newval == "" || newval == null) {
                     $scope.working[field] = undefined;
                     newval = null;
                 }
@@ -583,9 +597,77 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
         });
     }
 
+    $scope.working = {
+        statcats: {},
+        statcat_filter: undefined
+    };
+
+    $scope.statcatUpdate = function (id) {
+        var newval = $scope.working.statcats[id];
+
+        if (typeof newval != 'undefined') {
+            if (angular.isObject(newval)) { // we'll use the pkey
+                newval = newval.id();
+            }
+    
+            if (""+newval == "" || newval == null) {
+                $scope.working.statcats[id] = undefined;
+                newval = null;
+            }
+    
+            if (!$scope.in_item_select && $scope.workingGridControls && $scope.workingGridControls.selectedItems) {
+                angular.forEach(
+                    $scope.workingGridControls.selectedItems(),
+                    function (cp) {
+                        cp.stat_cat_entries(
+                            angular.forEach( cp.stat_cat_entries(), function (e) {
+                                if (e.stat_cat() == id) { // mark deleted
+                                    e.isdeleted(1);
+                                }
+                            })
+                        );
+    
+                        if (newval) {
+                            var e = new egCore.idl.ascecm();
+                            e.isnew( 1 );
+                            e.owning_copy( cp.id() );
+                            e.stat_cat( id );
+                            e.stat_cat_entry( newval );
+
+                            cp.stat_cat_entries(
+                                cp.stat_cat_entries().concat([ e ])
+                            );
+
+                        }
+
+                        cp.stat_cat_entries( // trim out ephemeral deleted ones
+                            cp.stat_cat_entries().filter(function (e) {
+                                if (Boolean(e.isnew())) {
+                                    if (Boolean(e.isdeleted())) {
+                                        return false;
+                                    }
+                                }
+                                return true;
+                            })
+                        );
+   
+                        cp.ischanged(1);
+                    }
+                );
+            }
+        }
+    }
+
     $scope.applyTemplate = function (n) {
         angular.forEach($scope.templates[n], function (v,k) {
-            $scope.working[k] = angular.copy(v);
+            if (!angular.isObject(v)) {
+                $scope.working[k] = angular.copy(v);
+            } else {
+                angular.forEach(v, function (sv,sk) {
+                    $scope.working[k][sk] = angular.copy(sv);
+                    if (k == 'statcats') $scope.statcatUpdate(sk);
+                });
+            }
         });
     }
 
@@ -608,8 +690,6 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
         }
         $scope.fetchTemplates();
  
-        $scope.working = {};
-
         $scope.copytab = 'working';
         $scope.tab = 'edit';
         $scope.summaryRecord = null;
@@ -618,6 +698,7 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
         $scope.completed_copies = [];
         $scope.location_orgs = [];
         $scope.location_cache = {};
+        $scope.statcats = [];
         if (!$scope.batch) $scope.batch = {};
 
         $scope.applyBatchCNValues = function () {
@@ -641,9 +722,17 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
 
         $scope.clearWorking = function () {
             angular.forEach($scope.working, function (v,k,o) {
-                if (typeof v != 'undefined')
-                    $scope.working[k] = undefined;
+                if (!angular.isObject(v)) {
+                    if (typeof v != 'undefined')
+                        $scope.working[k] = undefined;
+                } else if (k != 'circ_lib') {
+                    angular.forEach(v, function (sv,sk) {
+                        if (typeof v != 'undefined')
+                            $scope.working[k][sk] = undefined;
+                    });
+                }
             });
+            $scope.working.circ_lib = undefined; // special
         }
 
         $scope.completedGridDataProvider = egGridDataProvider.instance({
@@ -703,11 +792,82 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
             $scope.workingGridDataProvider.refresh();
         });
 
+        $scope.in_item_select = false;
+        $scope.afterItemSelect = function() { $scope.in_item_select = false };
+        $scope.handleItemSelect = function (item_list) {
+            if (item_list && item_list.length > 0) {
+                $scope.in_item_select = true;
+
+                angular.forEach(Object.keys($scope.defaults.attributes), function (attr) {
+
+                    var value_hash = {};
+                    angular.forEach(item_list, function (item) {
+                        if (item[attr]) {
+                            var v = item[attr]()
+                            if (angular.isObject(v)) {
+                                if (v.id) v = v.id();
+                                else if (v.code) v = v.code();
+                            }
+                            value_hash[v] = 1;
+                        }
+                    });
+
+                    if (Object.keys(value_hash).length == 1) {
+                        if (attr == 'circ_lib') {
+                            $scope.working[attr] = egCore.org.get(item_list[0][attr]());
+                        } else {
+                            $scope.working[attr] = item_list[0][attr]();
+                        }
+                    } else {
+                        $scope.working[attr] = undefined;
+                    }
+                });
+
+                angular.forEach($scope.statcats, function (sc) {
+
+                    var counter = -1;
+                    var value_hash = {};
+                    var none = false;
+                    angular.forEach(item_list, function (item) {
+                        if (item.stat_cat_entries().length > 0) {
+                            var right_sc = item.stat_cat_entries().filter(function (e) {
+                                return e.stat_cat() == sc.id() && !Boolean(e.isdeleted());
+                            });
+
+                            if (right_sc.length > 0) {
+                                value_hash[right_sc[0].stat_cat_entry()] = right_sc[0].stat_cat_entry();
+                            } else {
+                                none = true;
+                            }
+                        } else {
+                            none = true;
+                        }
+                    });
+
+                    if (!none && Object.keys(value_hash).length == 1) {
+                        $scope.working.statcats[sc.id()] = value_hash[Object.keys(value_hash)[0]];
+                    } else {
+                        $scope.working.statcats[sc.id()] = undefined;
+                    }
+                });
+
+            } else {
+                $scope.clearWorking();
+            }
+
+        }
+
         $scope.$watch('data.copies.length', function () {
             if ($scope.data.copies) {
                 var base_orgs = $scope.data.copies.map(function(cp){
                     return cp.circ_lib()
-                }).filter(function(e,i,a){
+                }).concat(
+                    $scope.data.copies.map(function(cp){
+                        return cp.call_number().owning_lib()
+                    })
+                ).concat(
+                    [egCore.auth.user().ws_ou()]
+                ).filter(function(e,i,a){
                     return a.lastIndexOf(e) === i;
                 });
 
@@ -729,6 +889,28 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
                             });
                             $scope.location_list = list;
                         });
+
+                        $scope.statcat_filter_list = [];
+                        angular.forEach($scope.location_orgs, function (o) {
+                            $scope.statcat_filter_list.push(egCore.org.get(o));
+                        });
+
+                        itemSvc.get_statcats($scope.location_orgs).then(function(list){
+                            $scope.statcats = list;
+                            angular.forEach($scope.statcats, function (s) {
+
+                                if (!$scope.working)
+                                    $scope.working = { statcats: {}, statcat_filter: undefined};
+                                if (!$scope.working.statcats)
+                                    $scope.working.statcats = {};
+
+                                if (!$scope.in_item_select) {
+                                    $scope.working.statcats[s.id()] = undefined;
+                                }
+                                createStatcatUpdateWatcher(s.id());
+                            });
+                            $scope.in_item_select = false;
+                        });
                     }
                 }
             }
@@ -811,6 +993,7 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
             function ( $scope , itemSvc , egCore ) {
 
                 $scope.defaults = { // If defaults are not set at all, allow everything
+                    statcats : true,
                     attributes : {
                         status : true,
                         loan_duration : true,
@@ -837,6 +1020,7 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
                     egCore.hatch.getItem('cat.copy.defaults').then(function(t) {
                         if (t) {
                             $scope.defaults = t;
+                            $scope.working.statcat_filter = $scope.defaults.statcat_filter;
                         }
                     });
                 }
@@ -857,10 +1041,16 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
             
                 $scope.applyTemplate = function (n) {
                     angular.forEach($scope.templates[n], function (v,k) {
-                        $scope.working[k] = angular.copy(v);
+                        if (!angular.isObject(v)) {
+                            $scope.working[k] = angular.copy(v);
+                        } else {
+                            angular.forEach(v, function (sv,sk) {
+                                $scope.working[k][sk] = angular.copy(sv);
+                            });
+                        }
                     });
                 }
-            
+
                 $scope.deleteTemplate = function (n) {
                     if (n) {
                         delete $scope.templates[n]
@@ -919,7 +1109,7 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
                                 else if (newval.code) $scope.working[field] = newval.code();
                             }
             
-                            if (newval == "") {
+                            if (""+newval == "" || newval == null) {
                                 $scope.working[field] = undefined;
                             }
             
@@ -927,13 +1117,45 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
                     });
                 }
             
+                $scope.working = {
+                    statcats: {},
+                    statcat_filter: undefined
+                };
+            
+                createStatcatUpdateWatcher = function (id) {
+                    return $scope.$watch('working.statcats[' + id + ']', function () {
+                        if ($scope.working.statcats) {
+                            var newval = $scope.working.statcats[id];
+                
+                            if (typeof newval != 'undefined') {
+                                if (angular.isObject(newval)) { // we'll use the pkey
+                                    newval = newval.id();
+                                }
+                
+                                if (""+newval == "" || newval == null) {
+                                    $scope.working.statcats[id] = undefined;
+                                    newval = null;
+                                }
+                
+                            }
+                        }
+                    });
+                }
+
                 $scope.clearWorking = function () {
-                    angular.forEach($scope.working, function (v,k) {
-                        if (typeof v != 'undefined')
-                            $scope.working[k] = undefined;
+                    angular.forEach($scope.working, function (v,k,o) {
+                        if (!angular.isObject(v)) {
+                            if (typeof v != 'undefined')
+                                $scope.working[k] = undefined;
+                        } else if (k != 'circ_lib') {
+                            angular.forEach(v, function (sv,sk) {
+                                $scope.working[k][sk] = undefined;
+                            });
+                        }
                     });
+                    $scope.working.circ_lib = undefined; // special
                 }
-            
+
                 $scope.working = {};
                 $scope.location_orgs = [];
                 $scope.location_cache = {};
@@ -945,6 +1167,25 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
                     $scope.location_list = list;
                 });
                 createSimpleUpdateWatcher('location');
+
+                $scope.statcat_filter_list = egCore.org.fullPath( egCore.auth.user().ws_ou() );
+
+                $scope.statcats = [];
+                itemSvc.get_statcats(
+                    egCore.org.fullPath( egCore.auth.user().ws_ou(), true )
+                ).then(function(list){
+                    $scope.statcats = list;
+                    angular.forEach($scope.statcats, function (s) {
+
+                        if (!$scope.working)
+                            $scope.working = { statcats: {}, statcat_filter: undefined};
+                        if (!$scope.working.statcats)
+                            $scope.working.statcats = {};
+
+                        $scope.working.statcats[s.id()] = undefined;
+                        createStatcatUpdateWatcher(s.id());
+                    });
+                });
             
                 $scope.status_list = [];
                 itemSvc.get_statuses().then(function(list){

commit c9a2dbf3e8894062b4b2fb7677aa0a4a80d6f2f4
Author: Mike Rylander <mrylander at gmail.com>
Date:   Fri Aug 28 17:42:41 2015 -0400

    webstaff: Enhance grid item selection API for external hooks
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/share/t_autogrid.tt2 b/Open-ILS/src/templates/staff/share/t_autogrid.tt2
index 7a85f51..aaf14aa 100644
--- a/Open-ILS/src/templates/staff/share/t_autogrid.tt2
+++ b/Open-ILS/src/templates/staff/share/t_autogrid.tt2
@@ -292,6 +292,7 @@
              consequences and is unnecessary, avoid it -->
         <div>
           <input type='checkbox' title="[% l('Select Row') %]"
+            ng-change="updateSelected()"
             ng-model="selected[indexValue(item)]"/>
         </div>
       </div>
diff --git a/Open-ILS/web/js/ui/default/staff/services/grid.js b/Open-ILS/web/js/ui/default/staff/services/grid.js
index fbbdb46..5e399b0 100644
--- a/Open-ILS/web/js/ui/default/staff/services/grid.js
+++ b/Open-ILS/web/js/ui/default/staff/services/grid.js
@@ -29,6 +29,12 @@ angular.module('egGridMod',
             // Reference to externally provided egGridDataProvider
             itemsProvider : '=',
 
+            // Reference to externally provided item-selection handler
+            onSelect : '=',
+
+            // Reference to externally provided after-item-selection handler
+            afterSelect : '=',
+
             // comma-separated list of supported or disabled grid features
             // supported features:
             //  startSelected : init the grid with all rows selected by default
@@ -652,19 +658,28 @@ angular.module('egGridMod',
 
             // selects or deselects an item, without affecting the others.
             // returns true if the item is selected; false if de-selected.
+            // we overwrite the object so that we can watch $scope.selected
             grid.toggleSelectOneItem = function(index) {
                 if ($scope.selected[index]) {
                     delete $scope.selected[index];
+                    $scope.selected = angular.copy($scope.selected);
                     return false;
                 } else {
-                    return $scope.selected[index] = true;
+                    $scope.selected[index] = true;
+                    $scope.selected = angular.copy($scope.selected);
+                    return true;
                 }
             }
 
+            $scope.updateSelected = function () { 
+                    return $scope.selected = angular.copy($scope.selected);
+            };
+
             grid.selectAllItems = function() {
                 angular.forEach($scope.items, function(item) {
                     $scope.selected[grid.indexValue(item)] = true
-                });
+                }); 
+                $scope.selected = angular.copy($scope.selected);
             }
 
             $scope.$watch('selectAll', function(newVal) {
@@ -675,6 +690,13 @@ angular.module('egGridMod',
                 }
             });
 
+            if ($scope.onSelect) {
+                $scope.$watch('selected', function(newVal) {
+                    $scope.onSelect(grid.getSelectedItems());
+                    if ($scope.afterSelect) $scope.afterSelect();
+                });
+            }
+
             // returns true if item1 appears in the list before item2;
             // false otherwise.  this is slightly more efficient that
             // finding the position of each then comparing them.
@@ -758,6 +780,7 @@ angular.module('egGridMod',
                             $scope.selected[curIdx] = true;
                             if (curIdx == index) break; // all done
                         }
+                        $scope.selected = angular.copy($scope.selected);
                     }
                         
                 } else {
@@ -979,6 +1002,7 @@ angular.module('egGridMod',
                 }).finally(function() { 
                     console.debug('egGrid.collect() complete');
                     grid.collecting = false 
+                    $scope.selected = angular.copy($scope.selected);
                 });
             }
 

commit 0c085fd8662cfc0e2c3a436cba1ba6f592c437ee
Author: Mike Rylander <mrylander at gmail.com>
Date:   Fri Aug 28 17:41:18 2015 -0400

    webstaff: Add copy stat cats and entries to pcrud
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml
index b5a44b6..37aab83 100644
--- a/Open-ILS/examples/fm_IDL.xml
+++ b/Open-ILS/examples/fm_IDL.xml
@@ -6072,7 +6072,7 @@ SELECT  usr,
 			<link field="bucket" reltype="has_a" key="id" map="" class="ccnb"/>
 		</links>
 	</class>
-	<class id="asc" controller="open-ils.cstore" oils_obj:fieldmapper="asset::stat_cat" oils_persist:tablename="asset.stat_cat" reporter:label="Asset Statistical Category">
+	<class id="asc" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="asset::stat_cat" oils_persist:tablename="asset.stat_cat" reporter:label="Asset Statistical Category">
 		<fields oils_persist:primary="id" oils_persist:sequence="asset.stat_cat_id_seq">
 			<field reporter:label="Entries" name="entries" oils_persist:virtual="true" reporter:datatype="link"/>
 			<field reporter:label="Stat Cat ID" name="id" reporter:datatype="id" reporter:selector="name"/>
@@ -6089,6 +6089,11 @@ SELECT  usr,
 			<link field="sip_field" reltype="has_a" key="field" map="" class="ascsf"/>
 			<link field="entries" reltype="has_many" key="stat_cat" map="" class="asce"/>
 		</links>
+		<permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+			<actions>
+				<retrieve permission="STAFF_LOGIN" global_required="true"/>
+			</actions>
+		</permacrud>
 	</class>
 	<class id="ac" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="actor::card" oils_persist:tablename="actor.card" reporter:label="Library Card">
 		<fields oils_persist:primary="id" oils_persist:sequence="actor.card_id_seq">
@@ -7151,7 +7156,7 @@ SELECT  usr,
 			<link field="item" reltype="has_a" key="id" map="" class="cbrebi"/>
 		</links>
 	</class>
-	<class id="asce" controller="open-ils.cstore" oils_obj:fieldmapper="asset::stat_cat_entry" oils_persist:tablename="asset.stat_cat_entry" reporter:label="Item Stat Cat Entry">
+	<class id="asce" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="asset::stat_cat_entry" oils_persist:tablename="asset.stat_cat_entry" reporter:label="Item Stat Cat Entry">
 		<fields oils_persist:primary="id" oils_persist:sequence="asset.stat_cat_entry_id_seq">
 			<field reporter:label="Entry ID" name="id" reporter:datatype="int" />
 			<field reporter:label="Entry Owner" name="owner" reporter:datatype="link"/>
@@ -7162,6 +7167,11 @@ SELECT  usr,
 			<link field="stat_cat" reltype="has_a" key="id" map="" class="asc"/>
 			<link field="owner" reltype="has_a" key="id" map="" class="aou"/>
 		</links>
+		<permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+			<actions>
+				<retrieve permission="STAFF_LOGIN" global_required="true"/>
+			</actions>
+		</permacrud>
 	</class>
     <class id="ascsf" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="asset::stat_cat_sip_fields" oils_persist:tablename="asset.stat_cat_sip_fields" reporter:label="SIP Statistical Category Field Identifier">
         <fields oils_persist:primary="field">

commit 8adca92befae05371a5707f933fe91efd7450692
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Fri Aug 28 21:18:04 2015 +0000

    LP#1489955: enable filtering authority search/browse by thesaurus
    
    This patch adds support for restricting the scope of an
    authority headings browse or authority record search by
    thesaurus.  For example, in SuperCat, the URL pattern
    
    /opac/extras/browse/marcxml/authority.subject/1/heart/0/20
    
    generates returns a browse of up to 20 authority records
    centered around the subject heading "heart".  With this patch,
    the URL pattern
    
    /opac/extras/browse/marcxml/authority.subject/1/heart/0/20/c
    
    restricts the browse to MeSH headings. Multiple thesauruses
    can be specified as well; for example, this URL pattern
    
    /opac/extras/browse/marcxml/authority.subject/1/heart/0/20/a,b
    
    browses in LCSH and LC Children's.
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/authority.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/authority.pm
index f5a86d1..7a27a7f 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/authority.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/authority.pm
@@ -279,7 +279,9 @@ __PACKAGE__->register_method(
             {name => "term", type => "string", desc => "Search term"},
             {name => "page", type => "number", desc => "Zero-based page number"},
             {name => "page_size", type => "number",
-                desc => "Number of records per page"}
+                desc => "Number of records per page"},
+            {name => "thesauruses", type => "string",
+                desc => "Comma-separated this of thesauruses to restrict search/browse to"},
         ],
         return => {
             desc => "A list of authority record IDs",
@@ -298,7 +300,7 @@ sub authority_in_db_browse_or_search {
         qq/
             SELECT
                 (SELECT record FROM authority.simple_heading WHERE id = func.heading)
-            FROM authority.$method(?, ?, ?, ?) func(heading)
+            FROM authority.$method(?, ?, ?, ?, ?) func(heading)
         /,
         {}, @args
     );
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/SuperCat.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/SuperCat.pm
index 7f53b53..da7f806 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/SuperCat.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/SuperCat.pm
@@ -281,6 +281,7 @@ sub generic_new_authorities_method {
     my $term = ''.shift;
     my $page = int(shift || 0);
     my $page_size = shift;
+    my $thesauruses = shift;
 
     # undef ok, but other non numbers not ok
     $page_size = int($page_size) if defined $page_size;
@@ -303,7 +304,7 @@ sub generic_new_authorities_method {
     my $storage = create OpenSRF::AppSession("open-ils.storage");
     my $list = $storage->request(
         "open-ils.storage.authority.in_db.browse_or_search",
-        $method, $what, $term, $page, $page_size
+        $method, $what, $term, $page, $page_size, $thesauruses
     )->gather(1);
 
     $storage->kill_me;
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/SuperCat.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/SuperCat.pm
index 521b66c..ce5effe 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/SuperCat.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/SuperCat.pm
@@ -1611,8 +1611,8 @@ sub string_browse {
     my $path = $cgi->path_info;
     $path =~ s/^\///og;
 
-    my ($format,$axis,$site,$string,$page,$page_size) = split '/', $path;
-    #warn " >>> $format -> $axis -> $site -> $string -> $page -> $page_size ";
+    my ($format,$axis,$site,$string,$page,$page_size,$thesauruses) = split '/', $path;
+    #warn " >>> $format -> $axis -> $site -> $string -> $page -> $page_size -> $thesauruses";
 
     return item_age_browse($apache) if ($axis eq 'item-age'); # short-circut to the item-age sub
 
@@ -1621,12 +1621,16 @@ sub string_browse {
     $site ||= $cgi->param('searchOrg');
     $page ||= $cgi->param('startPage') || 0;
     $page_size ||= $cgi->param('count') || 9;
+    $thesauruses //= '';
+    $thesauruses =~ s/\s//g;
+    # protect against cats bouncing on the comma key...
+    $thesauruses = join(',', grep { $_ ne '' } split /,/, $thesauruses); 
 
     $page = 0 if ($page !~ /^-?\d+$/);
     $page_size = 9 if $page_size !~ /^\d+$/;
 
-    my $prev = join('/', $base,$format,$axis,$site,$string,$page - 1,$page_size);
-    my $next = join('/', $base,$format,$axis,$site,$string,$page + 1,$page_size);
+    my $prev = join('/', $base,$format,$axis,$site,$string,$page - 1,$page_size,$thesauruses);
+    my $next = join('/', $base,$format,$axis,$site,$string,$page + 1,$page_size,$thesauruses);
 
     unless ($string and $axis and grep { $axis eq $_ } keys %browse_types) {
         warn "something's wrong...";
@@ -1650,7 +1654,8 @@ sub string_browse {
             $realaxis,
             $string,
             $page,
-            $page_size
+            $page_size,
+            $thesauruses
         )->gather(1);
     } else {
         $tree = $supercat->request(
@@ -1696,20 +1701,24 @@ sub string_startwith {
     my $path = $cgi->path_info;
     $path =~ s/^\///og;
 
-    my ($format,$axis,$site,$string,$page,$page_size) = split '/', $path;
-    #warn " >>> $format -> $axis -> $site -> $string -> $page -> $page_size ";
+    my ($format,$axis,$site,$string,$page,$page_size,$thesauruses) = split '/', $path;
+    #warn " >>> $format -> $axis -> $site -> $string -> $page -> $page_size -> $thesauruses ";
 
     my $status = [$cgi->param('status')];
     my $cpLoc = [$cgi->param('copyLocation')];
     $site ||= $cgi->param('searchOrg');
     $page ||= $cgi->param('startPage') || 0;
     $page_size ||= $cgi->param('count') || 9;
+    $thesauruses //= '';
+    $thesauruses =~ s/\s//g;
+    # protect against cats bouncing on the comma key...
+    $thesauruses = join(',', grep { $_ ne '' } split /,/, $thesauruses); 
 
     $page = 0 if ($page !~ /^-?\d+$/);
     $page_size = 9 if $page_size !~ /^\d+$/;
 
-    my $prev = join('/', $base,$format,$axis,$site,$string,$page - 1,$page_size);
-    my $next = join('/', $base,$format,$axis,$site,$string,$page + 1,$page_size);
+    my $prev = join('/', $base,$format,$axis,$site,$string,$page - 1,$page_size,$thesauruses);
+    my $next = join('/', $base,$format,$axis,$site,$string,$page + 1,$page_size,$thesauruses);
 
     unless ($string and $axis and grep { $axis eq $_ } keys %browse_types) {
         warn "something's wrong...";
@@ -1733,7 +1742,8 @@ sub string_startwith {
             $realaxis,
             $string,
             $page,
-            $page_size
+            $page_size,
+            $thesauruses
         )->gather(1);
     } else {
         $tree = $supercat->request(
diff --git a/Open-ILS/src/sql/Pg/011.schema.authority.sql b/Open-ILS/src/sql/Pg/011.schema.authority.sql
index 0cb742f..014b731 100644
--- a/Open-ILS/src/sql/Pg/011.schema.authority.sql
+++ b/Open-ILS/src/sql/Pg/011.schema.authority.sql
@@ -207,6 +207,20 @@ CREATE INDEX authority_full_rec_value_index ON authority.full_rec (value);
 
 CREATE RULE protect_authority_rec_delete AS ON DELETE TO authority.record_entry DO INSTEAD (UPDATE authority.record_entry SET deleted = TRUE WHERE OLD.id = authority.record_entry.id; DELETE FROM authority.full_rec WHERE record = OLD.id);
 
+CREATE OR REPLACE FUNCTION authority.extract_thesaurus( marcxml TEXT ) RETURNS TEXT AS $func$
+DECLARE
+    thes_code TEXT;
+BEGIN
+    thes_code := vandelay.marc21_extract_fixed_field(marcxml,'Subj');
+    IF thes_code IS NULL THEN
+        thes_code := '|';
+    ELSIF thes_code = 'z' THEN
+        thes_code := COALESCE( oils_xpath_string('//*[@tag="040"]/*[@code="f"][1]', marcxml), '' );
+    END IF;
+    RETURN thes_code;
+END;
+$func$ LANGUAGE PLPGSQL STABLE STRICT;
+
 -- Intended to be used in a unique index on authority.record_entry like so:
 -- CREATE UNIQUE INDEX unique_by_heading_and_thesaurus
 --   ON authority.record_entry (heading)
@@ -235,13 +249,6 @@ BEGIN
           LIMIT 1;
     END IF;
 
-    thes_code := vandelay.marc21_extract_fixed_field(marcxml,'Subj');
-    IF thes_code IS NULL THEN
-        thes_code := '|';
-    ELSIF thes_code = 'z' THEN
-        thes_code := COALESCE( oils_xpath_string('//*[@tag="040"]/*[@code="f"][1]', marcxml), '' );
-    END IF;
-
     heading_text := '';
     FOR acsaf IN SELECT * FROM authority.control_set_authority_field WHERE control_set = cset AND main_entry IS NULL LOOP
         tag_used := acsaf.tag;
@@ -291,6 +298,7 @@ BEGIN
         IF no_thesaurus IS TRUE THEN
             heading_text := tag_used || ' ' || public.naco_normalize(heading_text);
         ELSE
+            thes_code := authority.extract_thesaurus(marcxml);
             heading_text := tag_used || '_' || COALESCE(nfi_used,'-') || '_' || thes_code || ' ' || public.naco_normalize(heading_text);
         END IF;
     ELSE
@@ -307,7 +315,8 @@ CREATE TABLE authority.simple_heading (
     atag            INT         NOT NULL REFERENCES authority.control_set_authority_field (id),
     value           TEXT        NOT NULL,
     sort_value      TEXT        NOT NULL,
-    index_vector    tsvector    NOT NULL
+    index_vector    tsvector    NOT NULL,
+    thesaurus       TEXT
 );
 CREATE TRIGGER authority_simple_heading_fti_trigger
     BEFORE UPDATE OR INSERT ON authority.simple_heading
@@ -317,6 +326,7 @@ CREATE INDEX authority_simple_heading_index_vector_idx ON authority.simple_headi
 CREATE INDEX authority_simple_heading_value_idx ON authority.simple_heading (value);
 CREATE INDEX authority_simple_heading_sort_value_idx ON authority.simple_heading (sort_value);
 CREATE INDEX authority_simple_heading_record_idx ON authority.simple_heading (record);
+CREATE INDEX authority_simple_heading_thesaurus_idx ON authority.simple_heading (thesaurus);
 
 CREATE OR REPLACE FUNCTION authority.simple_heading_set( marcxml TEXT ) RETURNS SETOF authority.simple_heading AS $func$
 DECLARE
@@ -345,6 +355,7 @@ BEGIN
     END IF;
 
     res.record := auth_id;
+    res.thesaurus := authority.extract_thesaurus(marcxml);
 
     FOR acsaf IN SELECT * FROM authority.control_set_authority_field WHERE control_set = cset LOOP
 
@@ -613,7 +624,7 @@ $func$ LANGUAGE plpgsql;
 
 
 -- Support function used to find the pivot for alpha-heading-browse style searching
-CREATE OR REPLACE FUNCTION authority.simple_heading_find_pivot( a INT[], q TEXT ) RETURNS TEXT AS $$
+CREATE OR REPLACE FUNCTION authority.simple_heading_find_pivot( a INT[], q TEXT, thesauruses TEXT DEFAULT '' ) RETURNS TEXT AS $$
 DECLARE
     sort_value_row  RECORD;
     value_row       RECORD;
@@ -629,6 +640,10 @@ BEGIN
       FROM  authority.simple_heading ash
       WHERE ash.atag = ANY (a)
             AND ash.sort_value >= t_term
+            AND CASE thesauruses
+                WHEN '' THEN TRUE
+                ELSE ash.thesaurus = ANY(regexp_split_to_array(thesauruses, ','))
+                END
       ORDER BY rank DESC, ash.sort_value
       LIMIT 1;
 
@@ -639,6 +654,10 @@ BEGIN
       FROM  authority.simple_heading ash
       WHERE ash.atag = ANY (a)
             AND ash.value >= t_term
+            AND CASE thesauruses
+                WHEN '' THEN TRUE
+                ELSE ash.thesaurus = ANY(regexp_split_to_array(thesauruses, ','))
+                END
       ORDER BY rank DESC, ash.sort_value
       LIMIT 1;
 
@@ -650,7 +669,7 @@ BEGIN
 END;
 $$ LANGUAGE PLPGSQL;
 
-CREATE OR REPLACE FUNCTION authority.simple_heading_browse_center( atag_list INT[], q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9 ) RETURNS SETOF BIGINT AS $$
+CREATE OR REPLACE FUNCTION authority.simple_heading_browse_center( atag_list INT[], q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
 DECLARE
     pivot_sort_value    TEXT;
     boffset             INT DEFAULT 0;
@@ -659,7 +678,7 @@ DECLARE
     alimit              INT DEFAULT 0;
 BEGIN
 
-    pivot_sort_value := authority.simple_heading_find_pivot(atag_list,q);
+    pivot_sort_value := authority.simple_heading_find_pivot(atag_list,q,thesauruses);
 
     IF page = 0 THEN
         blimit := pagesize / 2;
@@ -688,6 +707,10 @@ BEGIN
                         row_number() over ()
                   FROM  authority.simple_heading ash
                   WHERE ash.atag = ANY (atag_list)
+                        AND CASE thesauruses
+                            WHEN '' THEN TRUE
+                            ELSE ash.thesaurus = ANY(regexp_split_to_array(thesauruses, ','))
+                            END
                         AND ash.sort_value < pivot_sort_value
                   ORDER BY ash.sort_value DESC
                   LIMIT blimit
@@ -701,6 +724,10 @@ BEGIN
             SELECT  ash.id
               FROM  authority.simple_heading ash
               WHERE ash.atag = ANY (atag_list)
+                    AND CASE thesauruses
+                        WHEN '' THEN TRUE
+                        ELSE ash.thesaurus = ANY(regexp_split_to_array(thesauruses, ','))
+                        END
                     AND ash.sort_value >= pivot_sort_value
               ORDER BY ash.sort_value
               LIMIT alimit
@@ -756,37 +783,37 @@ CREATE OR REPLACE FUNCTION authority.atag_authority_tags_refs(atag TEXT) RETURNS
 $$ LANGUAGE SQL;
 
 
-CREATE OR REPLACE FUNCTION authority.axis_browse_center( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9 ) RETURNS SETOF BIGINT AS $$
-    SELECT * FROM authority.simple_heading_browse_center(authority.axis_authority_tags($1), $2, $3, $4)
+CREATE OR REPLACE FUNCTION authority.axis_browse_center( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_center(authority.axis_authority_tags($1), $2, $3, $4, $5)
 $$ LANGUAGE SQL ROWS 10;
 
-CREATE OR REPLACE FUNCTION authority.btag_browse_center( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9 ) RETURNS SETOF BIGINT AS $$
-    SELECT * FROM authority.simple_heading_browse_center(authority.btag_authority_tags($1), $2, $3, $4)
+CREATE OR REPLACE FUNCTION authority.btag_browse_center( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_center(authority.btag_authority_tags($1), $2, $3, $4, $5)
 $$ LANGUAGE SQL ROWS 10;
 
-CREATE OR REPLACE FUNCTION authority.atag_browse_center( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9 ) RETURNS SETOF BIGINT AS $$
-    SELECT * FROM authority.simple_heading_browse_center(authority.atag_authority_tags($1), $2, $3, $4)
+CREATE OR REPLACE FUNCTION authority.atag_browse_center( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_center(authority.atag_authority_tags($1), $2, $3, $4, $5)
 $$ LANGUAGE SQL ROWS 10;
 
-CREATE OR REPLACE FUNCTION authority.axis_browse_center_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9 ) RETURNS SETOF BIGINT AS $$
-    SELECT * FROM authority.simple_heading_browse_center(authority.axis_authority_tags_refs($1), $2, $3, $4)
+CREATE OR REPLACE FUNCTION authority.axis_browse_center_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_center(authority.axis_authority_tags_refs($1), $2, $3, $4, $5)
 $$ LANGUAGE SQL ROWS 10;
 
-CREATE OR REPLACE FUNCTION authority.btag_browse_center_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9 ) RETURNS SETOF BIGINT AS $$
-    SELECT * FROM authority.simple_heading_browse_center(authority.btag_authority_tags_refs($1), $2, $3, $4)
+CREATE OR REPLACE FUNCTION authority.btag_browse_center_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_center(authority.btag_authority_tags_refs($1), $2, $3, $4, $5)
 $$ LANGUAGE SQL ROWS 10;
 
-CREATE OR REPLACE FUNCTION authority.atag_browse_center_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9 ) RETURNS SETOF BIGINT AS $$
-    SELECT * FROM authority.simple_heading_browse_center(authority.atag_authority_tags_refs($1), $2, $3, $4)
+CREATE OR REPLACE FUNCTION authority.atag_browse_center_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_center(authority.atag_authority_tags_refs($1), $2, $3, $4, $5)
 $$ LANGUAGE SQL ROWS 10;
 
 
-CREATE OR REPLACE FUNCTION authority.simple_heading_browse_top( atag_list INT[], q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+CREATE OR REPLACE FUNCTION authority.simple_heading_browse_top( atag_list INT[], q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
 DECLARE
     pivot_sort_value    TEXT;
 BEGIN
 
-    pivot_sort_value := authority.simple_heading_find_pivot(atag_list,q);
+    pivot_sort_value := authority.simple_heading_find_pivot(atag_list,q,thesauruses);
 
     IF page < 0 THEN
         RETURN QUERY
@@ -796,6 +823,10 @@ BEGIN
                         row_number() over ()
                   FROM  authority.simple_heading ash
                   WHERE ash.atag = ANY (atag_list)
+                        AND CASE thesauruses
+                            WHEN '' THEN TRUE
+                            ELSE ash.thesaurus = ANY(regexp_split_to_array(thesauruses, ','))
+                            END
                         AND ash.sort_value < pivot_sort_value
                   ORDER BY ash.sort_value DESC
                   LIMIT pagesize
@@ -809,6 +840,10 @@ BEGIN
             SELECT  ash.id
               FROM  authority.simple_heading ash
               WHERE ash.atag = ANY (atag_list)
+                AND CASE thesauruses
+                    WHEN '' THEN TRUE
+                    ELSE ash.thesaurus = ANY(regexp_split_to_array(thesauruses, ','))
+                    END
                     AND ash.sort_value >= pivot_sort_value
               ORDER BY ash.sort_value
               LIMIT pagesize
@@ -817,38 +852,42 @@ BEGIN
 END;
 $$ LANGUAGE PLPGSQL ROWS 10;
 
-CREATE OR REPLACE FUNCTION authority.axis_browse_top( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
-    SELECT * FROM authority.simple_heading_browse_top(authority.axis_authority_tags($1), $2, $3, $4)
+CREATE OR REPLACE FUNCTION authority.axis_browse_top( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_top(authority.axis_authority_tags($1), $2, $3, $4, $5)
 $$ LANGUAGE SQL ROWS 10;
 
-CREATE OR REPLACE FUNCTION authority.btag_browse_top( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
-    SELECT * FROM authority.simple_heading_browse_top(authority.btag_authority_tags($1), $2, $3, $4)
+CREATE OR REPLACE FUNCTION authority.btag_browse_top( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_top(authority.btag_authority_tags($1), $2, $3, $4, $5)
 $$ LANGUAGE SQL ROWS 10;
 
-CREATE OR REPLACE FUNCTION authority.atag_browse_top( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
-    SELECT * FROM authority.simple_heading_browse_top(authority.atag_authority_tags($1), $2, $3, $4)
+CREATE OR REPLACE FUNCTION authority.atag_browse_top( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_top(authority.atag_authority_tags($1), $2, $3, $4, $5)
 $$ LANGUAGE SQL ROWS 10;
 
-CREATE OR REPLACE FUNCTION authority.axis_browse_top_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
-    SELECT * FROM authority.simple_heading_browse_top(authority.axis_authority_tags_refs($1), $2, $3, $4)
+CREATE OR REPLACE FUNCTION authority.axis_browse_top_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_top(authority.axis_authority_tags_refs($1), $2, $3, $4, $5)
 $$ LANGUAGE SQL ROWS 10;
 
-CREATE OR REPLACE FUNCTION authority.btag_browse_top_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
-    SELECT * FROM authority.simple_heading_browse_top(authority.btag_authority_tags_refs($1), $2, $3, $4)
+CREATE OR REPLACE FUNCTION authority.btag_browse_top_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_top(authority.btag_authority_tags_refs($1), $2, $3, $4, $5)
 $$ LANGUAGE SQL ROWS 10;
 
-CREATE OR REPLACE FUNCTION authority.atag_browse_top_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
-    SELECT * FROM authority.simple_heading_browse_top(authority.atag_authority_tags_refs($1), $2, $3, $4)
+CREATE OR REPLACE FUNCTION authority.atag_browse_top_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_top(authority.atag_authority_tags_refs($1), $2, $3, $4, $5)
 $$ LANGUAGE SQL ROWS 10;
 
 
-CREATE OR REPLACE FUNCTION authority.simple_heading_search_rank( atag_list INT[], q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+CREATE OR REPLACE FUNCTION authority.simple_heading_search_rank( atag_list INT[], q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
     SELECT  ash.id
       FROM  authority.simple_heading ash,
             public.naco_normalize($2) t(term),
             plainto_tsquery('keyword'::regconfig,$2) ptsq(term)
       WHERE ash.atag = ANY ($1)
             AND ash.index_vector @@ ptsq.term
+            AND CASE thesauruses
+                WHEN '' THEN TRUE
+                ELSE ash.thesaurus = ANY(regexp_split_to_array(thesauruses, ','))
+                END
       ORDER BY ts_rank_cd(ash.index_vector,ptsq.term,14)::numeric
                     + CASE WHEN ash.sort_value LIKE t.term || '%' THEN 2 ELSE 0 END
                     + CASE WHEN ash.value LIKE t.term || '%' THEN 1 ELSE 0 END DESC
@@ -856,65 +895,69 @@ CREATE OR REPLACE FUNCTION authority.simple_heading_search_rank( atag_list INT[]
       OFFSET $4 * $3;
 $$ LANGUAGE SQL ROWS 10;
 
-CREATE OR REPLACE FUNCTION authority.axis_search_rank( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
-    SELECT * FROM authority.simple_heading_search_rank(authority.axis_authority_tags($1), $2, $3, $4)
+CREATE OR REPLACE FUNCTION authority.axis_search_rank( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_rank(authority.axis_authority_tags($1), $2, $3, $4, $5)
 $$ LANGUAGE SQL ROWS 10;
 
-CREATE OR REPLACE FUNCTION authority.btag_search_rank( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
-    SELECT * FROM authority.simple_heading_search_rank(authority.btag_authority_tags($1), $2, $3, $4)
+CREATE OR REPLACE FUNCTION authority.btag_search_rank( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_rank(authority.btag_authority_tags($1), $2, $3, $4, $5)
 $$ LANGUAGE SQL ROWS 10;
 
-CREATE OR REPLACE FUNCTION authority.atag_search_rank( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
-    SELECT * FROM authority.simple_heading_search_rank(authority.atag_authority_tags($1), $2, $3, $4)
+CREATE OR REPLACE FUNCTION authority.atag_search_rank( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_rank(authority.atag_authority_tags($1), $2, $3, $4, $5)
 $$ LANGUAGE SQL ROWS 10;
 
-CREATE OR REPLACE FUNCTION authority.axis_search_rank_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
-    SELECT * FROM authority.simple_heading_search_rank(authority.axis_authority_tags_refs($1), $2, $3, $4)
+CREATE OR REPLACE FUNCTION authority.axis_search_rank_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_rank(authority.axis_authority_tags_refs($1), $2, $3, $4, $5)
 $$ LANGUAGE SQL ROWS 10;
 
-CREATE OR REPLACE FUNCTION authority.btag_search_rank_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
-    SELECT * FROM authority.simple_heading_search_rank(authority.btag_authority_tags_refs($1), $2, $3, $4)
+CREATE OR REPLACE FUNCTION authority.btag_search_rank_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_rank(authority.btag_authority_tags_refs($1), $2, $3, $4, $5)
 $$ LANGUAGE SQL ROWS 10;
 
-CREATE OR REPLACE FUNCTION authority.atag_search_rank_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
-    SELECT * FROM authority.simple_heading_search_rank(authority.atag_authority_tags_refs($1), $2, $3, $4)
+CREATE OR REPLACE FUNCTION authority.atag_search_rank_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_rank(authority.atag_authority_tags_refs($1), $2, $3, $4, $5)
 $$ LANGUAGE SQL ROWS 10;
 
 
-CREATE OR REPLACE FUNCTION authority.simple_heading_search_heading( atag_list INT[], q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
+CREATE OR REPLACE FUNCTION authority.simple_heading_search_heading( atag_list INT[], q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
     SELECT  ash.id
       FROM  authority.simple_heading ash,
             public.naco_normalize($2) t(term),
             plainto_tsquery('keyword'::regconfig,$2) ptsq(term)
       WHERE ash.atag = ANY ($1)
             AND ash.index_vector @@ ptsq.term
+            AND CASE thesauruses
+                WHEN '' THEN TRUE
+                ELSE ash.thesaurus = ANY(regexp_split_to_array(thesauruses, ','))
+                END
       ORDER BY ash.sort_value
       LIMIT $4
       OFFSET $4 * $3;
 $$ LANGUAGE SQL ROWS 10;
 
-CREATE OR REPLACE FUNCTION authority.axis_search_heading( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
-    SELECT * FROM authority.simple_heading_search_heading(authority.axis_authority_tags($1), $2, $3, $4)
+CREATE OR REPLACE FUNCTION authority.axis_search_heading( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_heading(authority.axis_authority_tags($1), $2, $3, $4, $5)
 $$ LANGUAGE SQL ROWS 10;
 
-CREATE OR REPLACE FUNCTION authority.btag_search_heading( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
-    SELECT * FROM authority.simple_heading_search_heading(authority.btag_authority_tags($1), $2, $3, $4)
+CREATE OR REPLACE FUNCTION authority.btag_search_heading( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_heading(authority.btag_authority_tags($1), $2, $3, $4, $5)
 $$ LANGUAGE SQL ROWS 10;
 
-CREATE OR REPLACE FUNCTION authority.atag_search_heading( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
-    SELECT * FROM authority.simple_heading_search_heading(authority.atag_authority_tags($1), $2, $3, $4)
+CREATE OR REPLACE FUNCTION authority.atag_search_heading( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_heading(authority.atag_authority_tags($1), $2, $3, $4, $5)
 $$ LANGUAGE SQL ROWS 10;
 
-CREATE OR REPLACE FUNCTION authority.axis_search_heading_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
-    SELECT * FROM authority.simple_heading_search_heading(authority.axis_authority_tags_refs($1), $2, $3, $4)
+CREATE OR REPLACE FUNCTION authority.axis_search_heading_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_heading(authority.axis_authority_tags_refs($1), $2, $3, $4, $5)
 $$ LANGUAGE SQL ROWS 10;
 
-CREATE OR REPLACE FUNCTION authority.btag_search_heading_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
-    SELECT * FROM authority.simple_heading_search_heading(authority.btag_authority_tags_refs($1), $2, $3, $4)
+CREATE OR REPLACE FUNCTION authority.btag_search_heading_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_heading(authority.btag_authority_tags_refs($1), $2, $3, $4, $5)
 $$ LANGUAGE SQL ROWS 10;
 
-CREATE OR REPLACE FUNCTION authority.atag_search_heading_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10 ) RETURNS SETOF BIGINT AS $$
-    SELECT * FROM authority.simple_heading_search_heading(authority.atag_authority_tags_refs($1), $2, $3, $4)
+CREATE OR REPLACE FUNCTION authority.atag_search_heading_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_heading(authority.atag_authority_tags_refs($1), $2, $3, $4, $5)
 $$ LANGUAGE SQL ROWS 10;
 
 
diff --git a/Open-ILS/src/sql/Pg/999.functions.global.sql b/Open-ILS/src/sql/Pg/999.functions.global.sql
index 589eab8..d98edc0 100644
--- a/Open-ILS/src/sql/Pg/999.functions.global.sql
+++ b/Open-ILS/src/sql/Pg/999.functions.global.sql
@@ -1642,8 +1642,8 @@ BEGIN
 
     FOR ashs IN SELECT * FROM authority.simple_heading_set(NEW.marc) LOOP
 
-        INSERT INTO authority.simple_heading (record,atag,value,sort_value)
-            VALUES (ashs.record, ashs.atag, ashs.value, ashs.sort_value);
+        INSERT INTO authority.simple_heading (record,atag,value,sort_value,thesaurus)
+            VALUES (ashs.record, ashs.atag, ashs.value, ashs.sort_value, ashs.thesaurus);
             ash_id := CURRVAL('authority.simple_heading_id_seq'::REGCLASS);
 
         SELECT INTO mbe_row * FROM metabib.browse_entry
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.filter_authority_browse_search_by_thesaurus.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.filter_authority_browse_search_by_thesaurus.sql
new file mode 100644
index 0000000..b9084ec
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.filter_authority_browse_search_by_thesaurus.sql
@@ -0,0 +1,603 @@
+BEGIN;
+
+-- SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+
+CREATE OR REPLACE FUNCTION authority.extract_thesaurus( marcxml TEXT ) RETURNS TEXT AS $func$
+DECLARE
+    thes_code TEXT;
+BEGIN
+    thes_code := vandelay.marc21_extract_fixed_field(marcxml,'Subj');
+    IF thes_code IS NULL THEN
+        thes_code := '|';
+    ELSIF thes_code = 'z' THEN
+        thes_code := COALESCE( oils_xpath_string('//*[@tag="040"]/*[@code="f"][1]', marcxml), '' );
+    END IF;
+    RETURN thes_code;
+END;
+$func$ LANGUAGE PLPGSQL STABLE STRICT;
+
+-- Intended to be used in a unique index on authority.record_entry like so:
+-- CREATE UNIQUE INDEX unique_by_heading_and_thesaurus
+--   ON authority.record_entry (heading)
+--   WHERE deleted IS FALSE or deleted = FALSE;
+CREATE OR REPLACE FUNCTION authority.normalize_heading( marcxml TEXT, no_thesaurus BOOL ) RETURNS TEXT AS $func$
+DECLARE
+    acsaf           authority.control_set_authority_field%ROWTYPE;
+    tag_used        TEXT;
+    nfi_used        TEXT;
+    sf              TEXT;
+    sf_node         TEXT;
+    tag_node        TEXT;
+    thes_code       TEXT;
+    cset            INT;
+    heading_text    TEXT;
+    tmp_text        TEXT;
+    first_sf        BOOL;
+    auth_id         INT DEFAULT COALESCE(NULLIF(oils_xpath_string('//*[@tag="901"]/*[local-name()="subfield" and @code="c"]', marcxml), ''), '0')::INT; 
+BEGIN
+    SELECT control_set INTO cset FROM authority.record_entry WHERE id = auth_id;
+
+    IF cset IS NULL THEN
+        SELECT  control_set INTO cset
+          FROM  authority.control_set_authority_field
+          WHERE tag IN ( SELECT  UNNEST(XPATH('//*[starts-with(@tag,"1")]/@tag',marcxml::XML)::TEXT[]))
+          LIMIT 1;
+    END IF;
+
+    heading_text := '';
+    FOR acsaf IN SELECT * FROM authority.control_set_authority_field WHERE control_set = cset AND main_entry IS NULL LOOP
+        tag_used := acsaf.tag;
+        nfi_used := acsaf.nfi;
+        first_sf := TRUE;
+
+        FOR tag_node IN SELECT unnest(oils_xpath('//*[@tag="'||tag_used||'"]',marcxml)) LOOP
+            FOR sf_node IN SELECT unnest(oils_xpath('./*[contains("'||acsaf.sf_list||'", at code)]',tag_node)) LOOP
+
+                tmp_text := oils_xpath_string('.', sf_node);
+                sf := oils_xpath_string('./@code', sf_node);
+
+                IF first_sf AND tmp_text IS NOT NULL AND nfi_used IS NOT NULL THEN
+
+                    tmp_text := SUBSTRING(
+                        tmp_text FROM
+                        COALESCE(
+                            NULLIF(
+                                REGEXP_REPLACE(
+                                    oils_xpath_string('./@ind'||nfi_used, tag_node),
+                                    $$\D+$$,
+                                    '',
+                                    'g'
+                                ),
+                                ''
+                            )::INT,
+                            0
+                        ) + 1
+                    );
+
+                END IF;
+
+                first_sf := FALSE;
+
+                IF tmp_text IS NOT NULL AND tmp_text <> '' THEN
+                    heading_text := heading_text || E'\u2021' || sf || ' ' || tmp_text;
+                END IF;
+            END LOOP;
+
+            EXIT WHEN heading_text <> '';
+        END LOOP;
+
+        EXIT WHEN heading_text <> '';
+    END LOOP;
+
+    IF heading_text <> '' THEN
+        IF no_thesaurus IS TRUE THEN
+            heading_text := tag_used || ' ' || public.naco_normalize(heading_text);
+        ELSE
+            thes_code := authority.extract_thesaurus(marcxml);
+            heading_text := tag_used || '_' || COALESCE(nfi_used,'-') || '_' || thes_code || ' ' || public.naco_normalize(heading_text);
+        END IF;
+    ELSE
+        heading_text := 'NOHEADING_' || thes_code || ' ' || MD5(marcxml);
+    END IF;
+
+    RETURN heading_text;
+END;
+$func$ LANGUAGE PLPGSQL STABLE STRICT;
+
+ALTER TABLE authority.simple_heading ADD COLUMN thesaurus TEXT;
+CREATE INDEX authority_simple_heading_thesaurus_idx ON authority.simple_heading (thesaurus);
+
+CREATE OR REPLACE FUNCTION authority.simple_heading_set( marcxml TEXT ) RETURNS SETOF authority.simple_heading AS $func$
+DECLARE
+    res             authority.simple_heading%ROWTYPE;
+    acsaf           authority.control_set_authority_field%ROWTYPE;
+    tag_used        TEXT;
+    nfi_used        TEXT;
+    sf              TEXT;
+    cset            INT;
+    heading_text    TEXT;
+    joiner_text     TEXT;
+    sort_text       TEXT;
+    tmp_text        TEXT;
+    tmp_xml         TEXT;
+    first_sf        BOOL;
+    auth_id         INT DEFAULT COALESCE(NULLIF(oils_xpath_string('//*[@tag="901"]/*[local-name()="subfield" and @code="c"]', marcxml), ''), '0')::INT; 
+BEGIN
+
+    SELECT control_set INTO cset FROM authority.record_entry WHERE id = auth_id;
+
+    IF cset IS NULL THEN
+        SELECT  control_set INTO cset
+          FROM  authority.control_set_authority_field
+          WHERE tag IN ( SELECT  UNNEST(XPATH('//*[starts-with(@tag,"1")]/@tag',marcxml::XML)::TEXT[]))
+          LIMIT 1;
+    END IF;
+
+    res.record := auth_id;
+    res.thesaurus := authority.extract_thesaurus(marcxml);
+
+    FOR acsaf IN SELECT * FROM authority.control_set_authority_field WHERE control_set = cset LOOP
+
+        res.atag := acsaf.id;
+        tag_used := acsaf.tag;
+        nfi_used := acsaf.nfi;
+        joiner_text := COALESCE(acsaf.joiner, ' ');
+
+        FOR tmp_xml IN SELECT UNNEST(XPATH('//*[@tag="'||tag_used||'"]', marcxml::XML)::TEXT[]) LOOP
+
+            heading_text := COALESCE(
+                oils_xpath_string('./*[contains("'||acsaf.display_sf_list||'", at code)]', tmp_xml, joiner_text),
+                ''
+            );
+
+            IF nfi_used IS NOT NULL THEN
+
+                sort_text := SUBSTRING(
+                    heading_text FROM
+                    COALESCE(
+                        NULLIF(
+                            REGEXP_REPLACE(
+                                oils_xpath_string('./@ind'||nfi_used, tmp_xml::TEXT),
+                                $$\D+$$,
+                                '',
+                                'g'
+                            ),
+                            ''
+                        )::INT,
+                        0
+                    ) + 1
+                );
+
+            ELSE
+                sort_text := heading_text;
+            END IF;
+
+            IF heading_text IS NOT NULL AND heading_text <> '' THEN
+                res.value := heading_text;
+                res.sort_value := public.naco_normalize(sort_text);
+                res.index_vector = to_tsvector('keyword'::regconfig, res.sort_value);
+                RETURN NEXT res;
+            END IF;
+
+        END LOOP;
+
+    END LOOP;
+
+    RETURN;
+END;
+
+$func$ LANGUAGE PLPGSQL STABLE STRICT;
+-- AFTER UPDATE OR INSERT trigger for authority.record_entry
+CREATE OR REPLACE FUNCTION authority.indexing_ingest_or_delete () RETURNS TRIGGER AS $func$
+DECLARE
+    ashs    authority.simple_heading%ROWTYPE;
+    mbe_row metabib.browse_entry%ROWTYPE;
+    mbe_id  BIGINT;
+    ash_id  BIGINT;
+BEGIN
+
+    IF NEW.deleted IS TRUE THEN -- If this authority is deleted
+        DELETE FROM authority.bib_linking WHERE authority = NEW.id; -- Avoid updating fields in bibs that are no longer visible
+        DELETE FROM authority.full_rec WHERE record = NEW.id; -- Avoid validating fields against deleted authority records
+        DELETE FROM authority.simple_heading WHERE record = NEW.id;
+          -- Should remove matching $0 from controlled fields at the same time?
+
+        -- XXX What do we about the actual linking subfields present in
+        -- authority records that target this one when this happens?
+        DELETE FROM authority.authority_linking
+            WHERE source = NEW.id OR target = NEW.id;
+
+        RETURN NEW; -- and we're done
+    END IF;
+
+    IF TG_OP = 'UPDATE' THEN -- re-ingest?
+        PERFORM * FROM config.internal_flag WHERE name = 'ingest.reingest.force_on_same_marc' AND enabled;
+
+        IF NOT FOUND AND OLD.marc = NEW.marc THEN -- don't do anything if the MARC didn't change
+            RETURN NEW;
+        END IF;
+
+        -- Unless there's a setting stopping us, propagate these updates to any linked bib records
+        PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_authority_auto_update' AND enabled;
+
+        IF NOT FOUND THEN
+            PERFORM authority.propagate_changes(NEW.id);
+        END IF;
+	
+        DELETE FROM authority.simple_heading WHERE record = NEW.id;
+        DELETE FROM authority.authority_linking WHERE source = NEW.id;
+    END IF;
+
+    INSERT INTO authority.authority_linking (source, target, field)
+        SELECT source, target, field FROM authority.calculate_authority_linking(
+            NEW.id, NEW.control_set, NEW.marc::XML
+        );
+
+    FOR ashs IN SELECT * FROM authority.simple_heading_set(NEW.marc) LOOP
+
+        INSERT INTO authority.simple_heading (record,atag,value,sort_value,thesaurus)
+            VALUES (ashs.record, ashs.atag, ashs.value, ashs.sort_value, ashs.thesaurus);
+            ash_id := CURRVAL('authority.simple_heading_id_seq'::REGCLASS);
+
+        SELECT INTO mbe_row * FROM metabib.browse_entry
+            WHERE value = ashs.value AND sort_value = ashs.sort_value;
+
+        IF FOUND THEN
+            mbe_id := mbe_row.id;
+        ELSE
+            INSERT INTO metabib.browse_entry
+                ( value, sort_value ) VALUES
+                ( ashs.value, ashs.sort_value );
+
+            mbe_id := CURRVAL('metabib.browse_entry_id_seq'::REGCLASS);
+        END IF;
+
+        INSERT INTO metabib.browse_entry_simple_heading_map (entry,simple_heading) VALUES (mbe_id,ash_id);
+
+    END LOOP;
+
+    -- Flatten and insert the afr data
+    PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_authority_full_rec' AND enabled;
+    IF NOT FOUND THEN
+        PERFORM authority.reingest_authority_full_rec(NEW.id);
+        PERFORM * FROM config.internal_flag WHERE name = 'ingest.disable_authority_rec_descriptor' AND enabled;
+        IF NOT FOUND THEN
+            PERFORM authority.reingest_authority_rec_descriptor(NEW.id);
+        END IF;
+    END IF;
+
+    RETURN NEW;
+END;
+$func$ LANGUAGE PLPGSQL;
+
+DROP FUNCTION IF EXISTS authority.atag_search_heading_refs(TEXT, TEXT, INT, INT);
+DROP FUNCTION IF EXISTS authority.btag_search_heading_refs(TEXT, TEXT, INT, INT);
+DROP FUNCTION IF EXISTS authority.axis_search_heading_refs(TEXT, TEXT, INT, INT);
+DROP FUNCTION IF EXISTS authority.atag_search_heading(TEXT, TEXT, INT, INT);
+DROP FUNCTION IF EXISTS authority.btag_search_heading(TEXT, TEXT, INT, INT);
+DROP FUNCTION IF EXISTS authority.axis_search_heading(TEXT, TEXT, INT, INT);
+DROP FUNCTION IF EXISTS authority.simple_heading_search_heading(INT[], TEXT, INT, INT);
+DROP FUNCTION IF EXISTS authority.atag_search_rank_refs(TEXT, TEXT, INT, INT);
+DROP FUNCTION IF EXISTS authority.btag_search_rank_refs(TEXT, TEXT, INT, INT);
+DROP FUNCTION IF EXISTS authority.axis_search_rank_refs(TEXT, TEXT, INT, INT);
+DROP FUNCTION IF EXISTS authority.atag_search_rank(TEXT, TEXT, INT, INT);
+DROP FUNCTION IF EXISTS authority.btag_search_rank(TEXT, TEXT, INT, INT);
+DROP FUNCTION IF EXISTS authority.axis_search_rank(TEXT, TEXT, INT, INT);
+DROP FUNCTION IF EXISTS authority.simple_heading_search_rank(INT[], TEXT, INT, INT);
+DROP FUNCTION IF EXISTS authority.atag_browse_top_refs(TEXT, TEXT, INT, INT);
+DROP FUNCTION IF EXISTS authority.btag_browse_top_refs(TEXT, TEXT, INT, INT);
+DROP FUNCTION IF EXISTS authority.axis_browse_top_refs(TEXT, TEXT, INT, INT);
+DROP FUNCTION IF EXISTS authority.atag_browse_top(TEXT, TEXT, INT, INT);
+DROP FUNCTION IF EXISTS authority.btag_browse_top(TEXT, TEXT, INT, INT);
+DROP FUNCTION IF EXISTS authority.axis_browse_top(TEXT, TEXT, INT, INT);
+DROP FUNCTION IF EXISTS authority.simple_heading_browse_top(INT[], TEXT, INT, INT);
+DROP FUNCTION IF EXISTS authority.atag_browse_center_refs(TEXT, TEXT, INT, INT);
+DROP FUNCTION IF EXISTS authority.btag_browse_center_refs(TEXT, TEXT, INT, INT);
+DROP FUNCTION IF EXISTS authority.axis_browse_center_refs(TEXT, TEXT, INT, INT);
+DROP FUNCTION IF EXISTS authority.atag_browse_center(TEXT, TEXT, INT, INT);
+DROP FUNCTION IF EXISTS authority.btag_browse_center(TEXT, TEXT, INT, INT);
+DROP FUNCTION IF EXISTS authority.axis_browse_center(TEXT, TEXT, INT, INT);
+DROP FUNCTION IF EXISTS authority.simple_heading_browse_center(INT[], TEXT, INT, INT);
+DROP FUNCTION IF EXISTS authority.simple_heading_find_pivot(INT[], TEXT);
+
+-- Support function used to find the pivot for alpha-heading-browse style searching
+CREATE OR REPLACE FUNCTION authority.simple_heading_find_pivot( a INT[], q TEXT, thesauruses TEXT DEFAULT '' ) RETURNS TEXT AS $$
+DECLARE
+    sort_value_row  RECORD;
+    value_row       RECORD;
+    t_term          TEXT;
+BEGIN
+
+    t_term := public.naco_normalize(q);
+
+    SELECT  CASE WHEN ash.sort_value LIKE t_term || '%' THEN 1 ELSE 0 END
+                + CASE WHEN ash.value LIKE t_term || '%' THEN 1 ELSE 0 END AS rank,
+            ash.sort_value
+    INTO  sort_value_row
+    FROM  authority.simple_heading ash
+    WHERE ash.atag = ANY (a)
+            AND ash.sort_value >= t_term
+            AND CASE thesauruses
+                WHEN '' THEN TRUE
+                ELSE ash.thesaurus = ANY(regexp_split_to_array(thesauruses, ','))
+                END
+    ORDER BY rank DESC, ash.sort_value
+    LIMIT 1;
+
+    SELECT  CASE WHEN ash.sort_value LIKE t_term || '%' THEN 1 ELSE 0 END
+                + CASE WHEN ash.value LIKE t_term || '%' THEN 1 ELSE 0 END AS rank,
+            ash.sort_value
+    INTO  value_row
+    FROM  authority.simple_heading ash
+    WHERE ash.atag = ANY (a)
+            AND ash.value >= t_term
+            AND CASE thesauruses
+                WHEN '' THEN TRUE
+                ELSE ash.thesaurus = ANY(regexp_split_to_array(thesauruses, ','))
+                END
+    ORDER BY rank DESC, ash.sort_value
+    LIMIT 1;
+
+    IF value_row.rank > sort_value_row.rank THEN
+        RETURN value_row.sort_value;
+    ELSE
+        RETURN sort_value_row.sort_value;
+    END IF;
+END;
+$$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION authority.simple_heading_browse_center( atag_list INT[], q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+DECLARE
+    pivot_sort_value    TEXT;
+    boffset             INT DEFAULT 0;
+    aoffset             INT DEFAULT 0;
+    blimit              INT DEFAULT 0;
+    alimit              INT DEFAULT 0;
+BEGIN
+
+    pivot_sort_value := authority.simple_heading_find_pivot(atag_list,q,thesauruses);
+
+    IF page = 0 THEN
+        blimit := pagesize / 2;
+        alimit := blimit;
+
+        IF pagesize % 2 <> 0 THEN
+            alimit := alimit + 1;
+        END IF;
+    ELSE
+        blimit := pagesize;
+        alimit := blimit;
+
+        boffset := pagesize / 2;
+        aoffset := boffset;
+
+        IF pagesize % 2 <> 0 THEN
+            boffset := boffset + 1;
+        END IF;
+    END IF;
+
+    IF page <= 0 THEN
+        -- "bottom" half of the browse results
+        RETURN QUERY
+            SELECT id FROM (
+                SELECT  ash.id,
+                        row_number() over ()
+                FROM  authority.simple_heading ash
+                WHERE ash.atag = ANY (atag_list)
+                        AND CASE thesauruses
+                            WHEN '' THEN TRUE
+                            ELSE ash.thesaurus = ANY(regexp_split_to_array(thesauruses, ','))
+                            END
+                        AND ash.sort_value < pivot_sort_value
+                ORDER BY ash.sort_value DESC
+                LIMIT blimit
+                OFFSET ABS(page) * pagesize - boffset
+            ) x ORDER BY row_number DESC;
+    END IF;
+
+    IF page >= 0 THEN
+        -- "bottom" half of the browse results
+        RETURN QUERY
+            SELECT  ash.id
+            FROM  authority.simple_heading ash
+            WHERE ash.atag = ANY (atag_list)
+                    AND CASE thesauruses
+                        WHEN '' THEN TRUE
+                        ELSE ash.thesaurus = ANY(regexp_split_to_array(thesauruses, ','))
+                        END
+                    AND ash.sort_value >= pivot_sort_value
+            ORDER BY ash.sort_value
+            LIMIT alimit
+            OFFSET ABS(page) * pagesize - aoffset;
+    END IF;
+END;
+$$ LANGUAGE PLPGSQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.axis_browse_center( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_center(authority.axis_authority_tags($1), $2, $3, $4, $5)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.btag_browse_center( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_center(authority.btag_authority_tags($1), $2, $3, $4, $5)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.atag_browse_center( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_center(authority.atag_authority_tags($1), $2, $3, $4, $5)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.axis_browse_center_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_center(authority.axis_authority_tags_refs($1), $2, $3, $4, $5)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.btag_browse_center_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_center(authority.btag_authority_tags_refs($1), $2, $3, $4, $5)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.atag_browse_center_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 9, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_center(authority.atag_authority_tags_refs($1), $2, $3, $4, $5)
+$$ LANGUAGE SQL ROWS 10;
+
+
+CREATE OR REPLACE FUNCTION authority.simple_heading_browse_top( atag_list INT[], q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+DECLARE
+    pivot_sort_value    TEXT;
+BEGIN
+
+    pivot_sort_value := authority.simple_heading_find_pivot(atag_list,q,thesauruses);
+
+    IF page < 0 THEN
+         -- "bottom" half of the browse results
+        RETURN QUERY
+            SELECT id FROM (
+                SELECT  ash.id,
+                        row_number() over ()
+                FROM  authority.simple_heading ash
+                WHERE ash.atag = ANY (atag_list)
+                        AND CASE thesauruses
+                            WHEN '' THEN TRUE
+                            ELSE ash.thesaurus = ANY(regexp_split_to_array(thesauruses, ','))
+                            END
+                        AND ash.sort_value < pivot_sort_value
+                ORDER BY ash.sort_value DESC
+                LIMIT pagesize
+                OFFSET (ABS(page) - 1) * pagesize
+            ) x ORDER BY row_number DESC;
+    END IF;
+
+    IF page >= 0 THEN
+         -- "bottom" half of the browse results
+        RETURN QUERY
+            SELECT  ash.id
+            FROM  authority.simple_heading ash
+            WHERE ash.atag = ANY (atag_list)
+                AND CASE thesauruses
+                    WHEN '' THEN TRUE
+                    ELSE ash.thesaurus = ANY(regexp_split_to_array(thesauruses, ','))
+                    END
+                    AND ash.sort_value >= pivot_sort_value
+            ORDER BY ash.sort_value
+            LIMIT pagesize
+            OFFSET ABS(page) * pagesize ;
+    END IF;
+END;
+$$ LANGUAGE PLPGSQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.axis_browse_top( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_top(authority.axis_authority_tags($1), $2, $3, $4, $5)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.btag_browse_top( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_top(authority.btag_authority_tags($1), $2, $3, $4, $5)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.atag_browse_top( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_top(authority.atag_authority_tags($1), $2, $3, $4, $5)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.axis_browse_top_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_top(authority.axis_authority_tags_refs($1), $2, $3, $4, $5)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.btag_browse_top_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_top(authority.btag_authority_tags_refs($1), $2, $3, $4, $5)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.atag_browse_top_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_browse_top(authority.atag_authority_tags_refs($1), $2, $3, $4, $5)
+$$ LANGUAGE SQL ROWS 10;
+
+
+CREATE OR REPLACE FUNCTION authority.simple_heading_search_rank( atag_list INT[], q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT  ash.id
+      FROM  authority.simple_heading ash,
+            public.naco_normalize($2) t(term),
+            plainto_tsquery('keyword'::regconfig,$2) ptsq(term)
+      WHERE ash.atag = ANY ($1)
+            AND ash.index_vector @@ ptsq.term
+            AND CASE thesauruses
+                WHEN '' THEN TRUE
+                ELSE ash.thesaurus = ANY(regexp_split_to_array(thesauruses, ','))
+                END
+      ORDER BY ts_rank_cd(ash.index_vector,ptsq.term,14)::numeric
+                    + CASE WHEN ash.sort_value LIKE t.term || '%' THEN 2 ELSE 0 END
+                    + CASE WHEN ash.value LIKE t.term || '%' THEN 1 ELSE 0 END DESC
+      LIMIT $4
+      OFFSET $4 * $3;
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.axis_search_rank( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_rank(authority.axis_authority_tags($1), $2, $3, $4, $5)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.btag_search_rank( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_rank(authority.btag_authority_tags($1), $2, $3, $4, $5)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.atag_search_rank( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_rank(authority.atag_authority_tags($1), $2, $3, $4, $5)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.axis_search_rank_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_rank(authority.axis_authority_tags_refs($1), $2, $3, $4, $5)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.btag_search_rank_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_rank(authority.btag_authority_tags_refs($1), $2, $3, $4, $5)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.atag_search_rank_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_rank(authority.atag_authority_tags_refs($1), $2, $3, $4, $5)
+$$ LANGUAGE SQL ROWS 10;
+
+
+CREATE OR REPLACE FUNCTION authority.simple_heading_search_heading( atag_list INT[], q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT  ash.id
+      FROM  authority.simple_heading ash,
+            public.naco_normalize($2) t(term),
+            plainto_tsquery('keyword'::regconfig,$2) ptsq(term)
+      WHERE ash.atag = ANY ($1)
+            AND ash.index_vector @@ ptsq.term
+            AND CASE thesauruses
+                WHEN '' THEN TRUE
+                ELSE ash.thesaurus = ANY(regexp_split_to_array(thesauruses, ','))
+                END
+      ORDER BY ash.sort_value
+      LIMIT $4
+      OFFSET $4 * $3;
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.axis_search_heading( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_heading(authority.axis_authority_tags($1), $2, $3, $4, $5)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.btag_search_heading( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_heading(authority.btag_authority_tags($1), $2, $3, $4, $5)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.atag_search_heading( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_heading(authority.atag_authority_tags($1), $2, $3, $4, $5)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.axis_search_heading_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_heading(authority.axis_authority_tags_refs($1), $2, $3, $4, $5)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.btag_search_heading_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_heading(authority.btag_authority_tags_refs($1), $2, $3, $4, $5)
+$$ LANGUAGE SQL ROWS 10;
+
+CREATE OR REPLACE FUNCTION authority.atag_search_heading_refs( a TEXT, q TEXT, page INT DEFAULT 0, pagesize INT DEFAULT 10, thesauruses TEXT DEFAULT '' ) RETURNS SETOF BIGINT AS $$
+    SELECT * FROM authority.simple_heading_search_heading(authority.atag_authority_tags_refs($1), $2, $3, $4, $5)
+$$ LANGUAGE SQL ROWS 10;
+
+
+\qecho
+\qecho Updating the thesaurus codes in authority.simple_heading;
+\qecho This may take a while in databases with many authority records.
+\qecho
+UPDATE authority.simple_heading a
+SET thesaurus = authority.extract_thesaurus(b.marc)
+FROM authority.record_entry b
+WHERE a.record = b.id;
+
+COMMIT;

commit b1f25d1d3b93617adb6e498c5960c87db4038a01
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Fri Aug 28 00:48:17 2015 +0000

    webstaff: teach manage authorities about the new MARC editor
    
    At the moment, a new window is opened. It might be worth seeing
    if we can open a dijit.Dialog instead, but... that might be
    pushing our luck.
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/web/js/ui/default/cat/authority/list.js b/Open-ILS/web/js/ui/default/cat/authority/list.js
index ba62550..17b59a8 100644
--- a/Open-ILS/web/js/ui/default/cat/authority/list.js
+++ b/Open-ILS/web/js/ui/default/cat/authority/list.js
@@ -13,6 +13,7 @@ dojo.require("fieldmapper.Fieldmapper");
 dojo.require('openils.CGI');
 dojo.require('openils.PermaCrud');
 dojo.require('openils.XUL');
+dojo.require('openils.Util');
 dojo.require('openils.widget.OrgUnitFilteringSelect');
 dojo.require("openils.widget.PCrudAutocompleteBox");
 dojo.require("MARC.FixedFields");
@@ -285,6 +286,13 @@ function loadMarcEditor(pcrud, rec) {
     /* Setting an explicit height results in a super skinny window, so fix that up */
     var initWidth = self.outerWidth / 2;
 
+    if (openils.Util.inIframe()) {
+        initWidth = initHeight;
+        win = window.open('/eg/staff/cat/catalog/authority/' + rec.id() + '/marc_edit','',    // XXX version?
+            'chrome,resizable=yes,height=' + initHeight + ',width=' + initWidth);
+        return;
+    }
+
     /*
        To run in Firefox directly, must set signed.applets.codebase_principal_support
        to true in about:config

commit 85c199c63984620cd1a8ad31d112492c61d1b4c4
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Fri Aug 28 00:45:23 2015 +0000

    webstaff: add support for editing authority records
    
    This adds basic support for invoking the MARC editor on
    authority records, and fixes a couple bugs discovered
    along the way. The route currently supported is
    
    /cat/catalog/authority/:authority_id/marc_edit
    
    In the future, some sort of summary view of an authority
    record might be added, in which case the route
    "/cat/catalog/authority/:authority_id" is available.
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/catalog/t_authority.tt2 b/Open-ILS/src/templates/staff/cat/catalog/t_authority.tt2
new file mode 100644
index 0000000..cfacc96
--- /dev/null
+++ b/Open-ILS/src/templates/staff/cat/catalog/t_authority.tt2
@@ -0,0 +1,3 @@
+<div ng-show="authority_id" class="row col-md-12">
+  <eg-marc-edit-record dirty-flag="stop_unload" record-id="authority_id" record-type="are" />
+</div>
diff --git a/Open-ILS/src/templates/staff/cat/share/t_marcedit.tt2 b/Open-ILS/src/templates/staff/cat/share/t_marcedit.tt2
index 68a9ac5..f450316 100644
--- a/Open-ILS/src/templates/staff/cat/share/t_marcedit.tt2
+++ b/Open-ILS/src/templates/staff/cat/share/t_marcedit.tt2
@@ -114,7 +114,7 @@
             <eg-marc-edit-fixed-field fixed-field="SubjUse" record="record"></eg-marc-edit-fixed-field>
             <eg-marc-edit-fixed-field fixed-field="SerUse" record="record"></eg-marc-edit-fixed-field>
             <eg-marc-edit-fixed-field fixed-field="TypeSubd" record="record"></eg-marc-edit-fixed-field>
-            <eg-marc-edit-fixed-field fixed-field="GovAgn" record="record"></eg-marc-edit-fixed-field>
+            <eg-marc-edit-fixed-field fixed-field="GovtAgn" record="record"></eg-marc-edit-fixed-field>
             <eg-marc-edit-fixed-field fixed-field="RefStatus" record="record"></eg-marc-edit-fixed-field>
             <eg-marc-edit-fixed-field fixed-field="UpdStatus" record="record"></eg-marc-edit-fixed-field>
         </div>
diff --git a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
index 358e97e..c31adcc 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
@@ -78,6 +78,12 @@ angular.module('egCatalogApp', ['ui.bootstrap','ngRoute','egCoreMod','egGridMod'
         resolve : resolver
     });
 
+    $routeProvider.when('/cat/catalog/authority/:authority_id/marc_edit', {
+        templateUrl: './cat/catalog/t_authority',
+        controller: 'AuthorityCtrl',
+        resolve : resolver
+    });
+
     $routeProvider.otherwise({redirectTo : '/cat/catalog/index'});
 })
 
@@ -562,6 +568,19 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
 
 }])
 
+.controller('AuthorityCtrl',
+       ['$scope','$routeParams','$location','$window','$q','egCore',
+function($scope , $routeParams , $location , $window , $q , egCore) {
+
+    // set record ID on page load if available...
+    $scope.authority_id = $routeParams.authority_id;
+
+    if ($routeParams.authority_id) $scope.from_route = true;
+    else $scope.from_route = false;
+
+    $scope.stop_unload = false;
+}])
+
 .controller('URLVerifyCtrl',
        ['$scope','$location',
 function($scope , $location) {
diff --git a/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js b/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
index 8f52306..1345415 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/services/marcedit.js
@@ -469,7 +469,7 @@ angular.module('egMarcMod', ['egCoreMod', 'ui.bootstrap'])
                 $scope.controlfields = [];
                 $scope.datafields = [];
 
-                egTagTable.loadTagTable();
+                egTagTable.loadTagTable({ marcRecordType : $scope.record_type });
 
                 $scope.saveFlatTextMARC = function () {
                     $scope.record = new MARC21.Record({ marcbreaker : $scope.flat_text_marc });
diff --git a/Open-ILS/web/js/ui/default/staff/cat/services/tagtable.js b/Open-ILS/web/js/ui/default/staff/cat/services/tagtable.js
index f4d5e97..f726f2e 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/services/tagtable.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/services/tagtable.js
@@ -36,7 +36,7 @@ function($q,   egCore,   egAuth) {
                 fields.marcFormat = args.marcFormat;
             }
             if (args.marcRecordType) {
-                fields.marcFormat = service.normalizeRecordType(args.marcFormat);
+                fields.marcRecordType = service.normalizeRecordType(args.marcRecordType);
             }
         }
         var tt_key = 'current_tag_table_' + fields.marcFormat + '_' +

commit 99a17c4ad454ec4eac3394bb224bb3ad738fdf8f
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Thu Aug 27 22:23:23 2015 +0000

    webstaff: add routine for Dojo JS to detect if running in iframe
    
    Sometimes legacy Dojo interfaces may need to adjust their
    behavior if they're embedded in the web staff client. This patch
    adds a new utility function, openils.Util.inIFrame(), to detect
    if such code is running in an iframe. If so, the presumption
    is that iframe belongs to the web staff client.
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/web/js/dojo/openils/Util.js b/Open-ILS/web/js/dojo/openils/Util.js
index 804d13c..e3588ae 100644
--- a/Open-ILS/web/js/dojo/openils/Util.js
+++ b/Open-ILS/web/js/dojo/openils/Util.js
@@ -494,5 +494,19 @@ if(!dojo._hasResource["openils.Util"]) {
         );
     };
 
+    /**
+     * Check to see if we're running in an iframe; if we
+     * are, we assume that the iframe is specifically one
+     * used by the web staff client to embed legacy interfaces.
+     */
+    openils.Util.inIframe = function() {
+        /* http://stackoverflow.com/a/326076 */
+        try {
+            return window.self !== window.top;
+        } catch (e) {
+            return true;
+        }
+    };
+
 }
 

commit 30514d239eb4d7860ed27f202bd4e9d30c1ad411
Author: Mike Rylander <mrylander at gmail.com>
Date:   Fri Aug 21 13:30:06 2015 -0400

    webstaff: Add actions for editing just copy attributes or vol/copy details
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2 b/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2
index 4220630..8cf4396 100644
--- a/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2
+++ b/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2
@@ -45,8 +45,12 @@
       label="[% l('Item as Damaged') %]"></eg-grid-action>
     <eg-grid-action handler="selectedHoldingsMissing" group="[% l('Mark') %]"
       label="[% l('Item as Missing') %]"></eg-grid-action>
+    <eg-grid-action handler="selectedHoldingsVolEdit" group="[% l('Edit') %]"
+      label="[% l('Selected Volumes') %]"></eg-grid-action>
+    <eg-grid-action handler="selectedHoldingsCopyEdit" group="[% l('Edit') %]"
+      label="[% l('Selected Copies') %]"></eg-grid-action>
     <eg-grid-action handler="selectedHoldingsVolCopyEdit" group="[% l('Edit') %]"
-      label="[% l('Selected Vols/Copies') %]"></eg-grid-action>
+      label="[% l('Selected Volumes and Copies') %]"></eg-grid-action>
 
     <eg-grid-field label="[% l('Owning Library') %]"  path="owner_label" flex="4" align="right" visible></eg-grid-field>
     <eg-grid-field label="[% l('Call Number') %]"     path="call_number.label" visible></eg-grid-field>
diff --git a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
index 604a955..358e97e 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
@@ -312,11 +312,16 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
         return cp_id_list;
     }
 
-    $scope.selectedHoldingsVolCopyEdit = function (){
+    spawnHoldingsEdit = function (hide_vols,hide_copies){
         egCore.net.request(
             'open-ils.actor',
             'open-ils.actor.anon_cache.set_value',
-            null, 'edit-these-copies', {record_id: $scope.record_id, copies: gatherSelectedHoldingsIds() }
+            null, 'edit-these-copies', {
+                record_id: $scope.record_id,
+                copies: gatherSelectedHoldingsIds(),
+                hide_vols : hide_vols,
+                hide_copies : hide_copies
+            }
         ).then(function(key) {
             if (key) {
                 var url = egCore.env.basePath + 'cat/volcopy/' + key;
@@ -326,6 +331,9 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
             }
         });
     }
+    $scope.selectedHoldingsVolCopyEdit = function () { spawnHoldingsEdit(false,false) }
+    $scope.selectedHoldingsVolEdit = function () { spawnHoldingsEdit(false,true) }
+    $scope.selectedHoldingsCopyEdit = function () { spawnHoldingsEdit(true,false) }
 
     $scope.selectedHoldingsItemStatus = function (){
         var url = egCore.env.basePath + 'cat/item/search/' + gatherSelectedHoldingsIds().join(',')

commit dbf2a4c73bc42ff23921d3ae757d648d3b0ef924
Author: Mike Rylander <mrylander at gmail.com>
Date:   Fri Aug 21 13:23:37 2015 -0400

    webstaff: Disable org selector when defaults say to
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2 b/Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2
index 92a9abe..e8fe8bc 100644
--- a/Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2
+++ b/Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2
@@ -84,7 +84,13 @@
 
             <div class="row">
                 <div class="col-md-6" ng-class="{'bg-success': working.circ_lib !== undefined}">
-                    <eg-org-selector selected="working.circ_lib" noDefault label="[% l('(Unset)') %]" disableTest="cant_have_vols"></eg-org-selector>
+                    <eg-org-selector
+                        alldisabled="{{!defaults.attributes.circ_lib}}"
+                        selected="working.circ_lib"
+                        noDefault
+                        label="[% l('(Unset)') %]"
+                        disableTest="cant_have_vols"
+                    ></eg-org-selector>
                 </div>
                 <div class="col-md-6" ng-class="{'bg-success': working.ref !== undefined}">
                     <div class="row">

commit 00d828090abc0dfe6d72525e91aba4b61cfc51ab
Author: Mike Rylander <mrylander at gmail.com>
Date:   Fri Aug 21 13:23:12 2015 -0400

    webstaff: Teach org selector to be completely disabled via text attr
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/services/ui.js b/Open-ILS/web/js/ui/default/staff/services/ui.js
index 966b3e3..aa5d0b7 100644
--- a/Open-ILS/web/js/ui/default/staff/services/ui.js
+++ b/Open-ILS/web/js/ui/default/staff/services/ui.js
@@ -259,6 +259,9 @@ function($modal, $interpolate) {
             // org unit will not be available for selection.
             disableTest : '=',
 
+            // if set to true, disable the UI element altogether
+            alldisabled : '@',
+
             // Caller can either $watch(selected, ..) or register an
             // onchange handler.
             onchange : '=',
@@ -270,7 +273,7 @@ function($modal, $interpolate) {
         // any reason to move this into a TT2 template?
         template : 
             '<div class="btn-group eg-org-selector" dropdown>'
-            + '<button type="button" class="btn btn-default dropdown-toggle">'
+            + '<button type="button" class="btn btn-default dropdown-toggle" ng-disabled="disable_button">'
              + '<span style="padding-right: 5px;">{{getSelectedName()}}</span>'
              + '<span class="caret"></span>'
            + '</button>'
@@ -287,6 +290,12 @@ function($modal, $interpolate) {
         controller : ['$scope','$timeout','egOrg','egAuth',
               function($scope , $timeout , egOrg , egAuth) {
 
+            if ($scope.alldisabled) {
+                $scope.disable_button = $scope.alldisabled == 'true' ? true : false;
+            } else {
+                $scope.disable_button = false;
+            }
+
             $scope.egOrg = egOrg; // for use in the link function
             $scope.egAuth = egAuth; // for use in the link function
 

commit 3491b0b9fb27cedf215aa962657740055c6f4f61
Author: Mike Rylander <mrylander at gmail.com>
Date:   Fri Aug 21 13:11:49 2015 -0400

    webstaff: Supply a "defaults" interface for disabling element and setting call number batch defaults
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2 b/Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2
index 509a3da..92a9abe 100644
--- a/Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2
+++ b/Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2
@@ -51,13 +51,13 @@
                     <div class="row">
                         <div class="col-xs-3">
                             <label>
-                                <input type="radio" ng-model="working.circulate" value="t"/>
+                                <input type="radio" ng-disabled="!defaults.attributes.circulate" ng-model="working.circulate" value="t"/>
                                 [% l('Yes') %]
                             </label>
                         </div>
                         <div class="col-xs-3">
                             <label>
-                                <input type="radio" ng-model="working.circulate" value="f"/>
+                                <input type="radio" ng-disabled="!defaults.attributes.circulate" ng-model="working.circulate" value="f"/>
                                 [% l('No') %]
                             </label>
                         </div>
@@ -65,7 +65,7 @@
                 </div>
                 <div class="col-md-6" ng-class="{'bg-success': working.status !== undefined}">
                     <select class="form-control"
-                        ng-model="working.status"
+                        ng-disabled="!defaults.attributes.status" ng-model="working.status"
                         ng-options="s.id() as s.name() for s in status_list"
                     ></select>
                 </div>
@@ -90,13 +90,13 @@
                     <div class="row">
                         <div class="col-xs-3">
                             <label>
-                                <input type="radio" ng-model="working.ref" value="t"/>
+                                <input type="radio" ng-disabled="!defaults.attributes.ref" ng-model="working.ref" value="t"/>
                                 [% l('Yes') %]
                             </label>
                         </div>
                         <div class="col-xs-3">
                             <label>
-                                <input type="radio" ng-model="working.ref" value="f"/>
+                                <input type="radio" ng-disabled="!defaults.attributes.ref" ng-model="working.ref" value="f"/>
                                 [% l('No') %]
                             </label>
                         </div>
@@ -118,7 +118,7 @@
             <div class="row">
                 <div class="col-md-6" ng-class="{'bg-success': working.location !== undefined}">
                     <select class="form-control"
-                        ng-model="working.location"
+                        ng-disabled="!defaults.attributes.location" ng-model="working.location"
                         ng-options="l.id() as l.name() for l in location_list"
                     ></select>
                 </div>
@@ -126,13 +126,13 @@
                     <div class="row">
                         <div class="col-xs-3">
                             <label>
-                                <input type="radio" ng-model="working.opac_visible" value="t"/>
+                                <input type="radio" ng-disabled="!defaults.attributes.opac_visible" ng-model="working.opac_visible" value="t"/>
                                 [% l('Yes') %]
                             </label>
                         </div>
                         <div class="col-xs-3">
                             <label>
-                                <input type="radio" ng-model="working.opac_visible" value="f"/>
+                                <input type="radio" ng-disabled="!defaults.attributes.opac_visible" ng-model="working.opac_visible" value="f"/>
                                 [% l('No') %]
                             </label>
                         </div>
@@ -154,14 +154,14 @@
             <div class="row">
                 <div class="col-md-6" ng-class="{'bg-success': working.circ_modifier !== undefined}">
                     <select class="form-control"
-                        ng-model="working.circ_modifier"
+                        ng-disabled="!defaults.attributes.circ_modifier" ng-model="working.circ_modifier"
                         ng-options="m.code() as m.name() for m in circ_modifier_list"
                     >
                         <option value="">[% l('<NONE>') %]</option>
                     </select>
                 </div>
                 <div class="col-md-6" ng-class="{'bg-success': working.price !== undefined}">
-                    <input class="form-control" ng-model="working.price" type="text"/>
+                    <input class="form-control" ng-disabled="!defaults.attributes.price" ng-model="working.price" type="text"/>
                 </div>
             </div>
 
@@ -178,14 +178,14 @@
 
             <div class="row">
                 <div class="col-md-6" ng-class="{'bg-success': working.loan_duration !== undefined}">
-                    <select class="form-control" ng-model="working.loan_duration">
+                    <select class="form-control" ng-disabled="!defaults.attributes.loan_duration" ng-model="working.loan_duration">
                         <option value="1">[% l('Short') %]</option>
                         <option value="2" selected>[% l('Normal') %]</option>
                         <option value="3">[% l('Extended') %]</option>
                     </select>
                 </div>
                 <div class="col-md-6" ng-class="{'bg-success': working.cost !== undefined}">
-                    <input class="form-control" ng-model="working.cost" type="text"/>
+                    <input class="form-control" ng-disabled="!defaults.attributes.cost" ng-model="working.cost" type="text"/>
                 </div>
             </div>
 
@@ -203,7 +203,7 @@
             <div class="row">
                 <div class="col-md-6" ng-class="{'bg-success': working.circ_as_type !== undefined}">
                     <select class="form-control"
-                        ng-model="working.circ_as_type"
+                        ng-disabled="!defaults.attributes.circ_as_type" ng-model="working.circ_as_type"
                         ng-options="t.code() as t.value() for t in circ_type_list"
                     ></select>
                 </div>
@@ -211,13 +211,13 @@
                     <div class="row">
                         <div class="col-xs-3">
                             <label>
-                                <input type="radio" ng-model="working.deposit" value="t"/>
+                                <input type="radio" ng-disabled="!defaults.attributes.deposit" ng-model="working.deposit" value="t"/>
                                 [% l('Yes') %]
                             </label>
                         </div>
                         <div class="col-xs-3">
                             <label>
-                                <input type="radio" ng-model="working.deposit" value="f"/>
+                                <input type="radio" ng-disabled="!defaults.attributes.deposit" ng-model="working.deposit" value="f"/>
                                 [% l('No') %]
                             </label>
                         </div>
@@ -241,20 +241,20 @@
                     <div class="row">
                         <div class="col-xs-3">
                             <label>
-                                <input type="radio" ng-model="working.holdable" value="t"/>
+                                <input type="radio" ng-disabled="!defaults.attributes.holdable" ng-model="working.holdable" value="t"/>
                                 [% l('Yes') %]
                             </label>
                         </div>
                         <div class="col-xs-3">
                             <label>
-                                <input type="radio" ng-model="working.holdable" value="f"/>
+                                <input type="radio" ng-disabled="!defaults.attributes.holdable" ng-model="working.holdable" value="f"/>
                                 [% l('No') %]
                             </label>
                         </div>
                     </div>
                 </div>
                 <div class="col-md-6" ng-class="{'bg-success': working.deposit_amount !== undefined}">
-                    <input class="form-control" ng-model="working.deposit_amount" type="text"/>
+                    <input class="form-control" ng-disabled="!defaults.attributes.deposit_amount" ng-model="working.deposit_amount" type="text"/>
                 </div>
             </div>
 
@@ -272,7 +272,7 @@
             <div class="row">
                 <div class="col-md-6" ng-class="{'bg-success': working.age_protect !== undefined}">
                     <select class="form-control"
-                        ng-model="working.age_protect"
+                        ng-disabled="!defaults.attributes.age_protect" ng-model="working.age_protect"
                         ng-options="a.id() as a.name() for a in age_protect_list"
                     ></select>
                 </div>
@@ -280,13 +280,13 @@
                     <div class="row">
                         <div class="col-xs-3">
                             <label>
-                                <input type="radio" ng-model="working.mint_condition" value="t"/>
+                                <input type="radio" ng-disabled="!defaults.attributes.mint_condition" ng-model="working.mint_condition" value="t"/>
                                 [% l('Good') %]
                             </label>
                         </div>
                         <div class="col-xs-3">
                             <label>
-                                <input type="radio" ng-model="working.mint_condition" value="f"/>
+                                <input type="radio" ng-disabled="!defaults.attributes.mint_condition" ng-model="working.mint_condition" value="f"/>
                                 [% l('Damaged') %]
                             </label>
                         </div>
@@ -304,7 +304,7 @@
 
             <div class="row">
                 <div class="col-md-6" ng-class="{'bg-success': working.fine_level !== undefined}">
-                    <select class="form-control" ng-model="working.fine_level">
+                    <select class="form-control" ng-disabled="!defaults.attributes.fine_level" ng-model="working.fine_level">
                         <option value="1">[% l('Low') %]</option>
                         <option value="2" selected>[% l('Normal') %]</option>
                         <option value="3">[% l('High') %]</option>
diff --git a/Open-ILS/src/templates/staff/cat/volcopy/t_defaults.tt2 b/Open-ILS/src/templates/staff/cat/volcopy/t_defaults.tt2
new file mode 100644
index 0000000..9f74e13
--- /dev/null
+++ b/Open-ILS/src/templates/staff/cat/volcopy/t_defaults.tt2
@@ -0,0 +1,291 @@
+<div class="container-fluid">
+    <div class="row">
+        <div class="col-md-3">
+            <div class="row">
+                <div class="col-xs-12">
+                    <h4>[% l('Volume/Copy Detail defaults') %]</h4>
+                    <label>
+                        <input type="checkbox" ng-change="saveDefaults()" ng-model="defaults.always_volumes"/>
+                        [% l('Always display Volume/Copy Detail pane') %]
+                    </label>
+                </div>
+            </div>
+
+            <div class="row">
+                <div class="col-xs-12">
+                    <h4 class="pad-vert">[% l('Creation Defaults') %]</h4>
+                    <select class="form-control" ng-change="saveDefaults()" ng-model="defaults.classification" ng-options="cl.id() as cl.name() for cl in classification_list">
+                        <option value="">Unset Default Classification Scheme</option>
+                    </select>
+                </div>
+            </div>
+
+            <div class="row">
+                <div class="col-xs-12">
+                    <select class="form-control" ng-change="saveDefaults()" ng-model="defaults.prefix" ng-options="p.id() as p.label() for p in prefix_list">
+                        <option value="">Unset Default Prefix</option>
+                    </select>
+                </div>
+            </div>
+
+            <div class="row">
+                <div class="col-xs-12">
+                    <select class="form-control" ng-change="saveDefaults()" ng-model="defaults.suffix" ng-options="s.id() as s.label() for s in suffix_list">
+                        <option value="">Unset Default Suffix</option>
+                    </select>
+                </div>
+            </div>
+
+            <div class="row pad-vert">
+                <div class="col-xs-12">
+                    <label>
+                        <input type="checkbox" ng-change="saveDefaults()" ng-model="defaults.auto_gen_barcode"/>
+                        [% l('Auto-generate Barcodes') %]
+                    </label>
+                </div>
+            </div>
+
+            <div class="row">
+                <div class="col-xs-12">
+                    <label style="padding-left: 25px">
+                        <input type="checkbox" ng-change="saveDefaults()" ng-model="defaults.barcode_checkdigit"/>
+                        [% l('Use checkdigit') %]
+                    </label>
+                </div>
+            </div>
+
+            <div class="row pad-vert">
+                <div class="col-xs-12">
+                    <label>
+                        <input type="checkbox" ng-change="saveDefaults()" ng-model="defaults.print_spine_labels"/>
+                        [% l('Print Spine Labels') %]
+                    </label>
+                </div>
+            </div>
+
+        </div>
+
+        <div class="col-md-5">
+            <div class="row">
+                <div class="col-xs-12">
+                    <h4>[% l('Display defaults for Working Copy tab') %]</h4>
+                </div>
+            </div>
+
+            <div class="row">
+                <div class="col-xs-6">
+                    <h6>[% l('Copy display') %]</h6>
+                </div>
+                <div class="col-xs-6">
+                    <h6>[% l('Miscellaneous') %]</h6>
+                </div>
+            </div>
+
+            <div class="row">
+                <div class="col-xs-6">
+                    <label>
+                        <input type="checkbox" ng-change="workingGridControls.saveConfig()" ng-model="workingGridControls.columnMap()['active_date'].visible"/>
+                        [% l('Activated') %]
+                    </label>
+                </div>
+                <div class="col-xs-6">
+                    <label>
+                        <input type="checkbox" ng-change="saveDefaults()" ng-model="defaults.attributes.alerts"/>
+                        [% l('Alerts') %]
+                    </label>
+                </div>
+            </div>
+
+            <div class="row">
+                <div class="col-xs-6">
+                    <label>
+                        <input type="checkbox" ng-change="workingGridControls.saveConfig()" ng-model="workingGridControls.columnMap()['create_date'].visible"/>
+                        [% l('Created') %]
+                    </label>
+                </div>
+                <div class="col-xs-6">
+                    <label>
+                        <input type="checkbox" ng-change="saveDefaults()" ng-model="defaults.attributes.deposit"/>
+                        [% l('Deposit?') %]
+                    </label>
+                </div>
+            </div>
+
+            <div class="row">
+                <div class="col-xs-6">
+                    <label>
+                        <input type="checkbox" ng-change="workingGridControls.saveConfig()" ng-model="workingGridControls.columnMap()['call_number.label'].visible"/>
+                        [% l('Call Number') %]
+                    </label>
+                </div>
+                <div class="col-xs-6">
+                    <label>
+                        <input type="checkbox" ng-change="saveDefaults()" ng-model="defaults.attributes.deposit_amount"/>
+                        [% l('Deposit Amount') %]
+                    </label>
+                </div>
+            </div>
+
+            <div class="row">
+                <div class="col-xs-6">
+                </div>
+                <div class="col-xs-6">
+                    <label>
+                        <input type="checkbox" ng-change="saveDefaults()" ng-model="defaults.attributes.opac_visible"/>
+                        [% l('OPAC Visible?') %]
+                    </label>
+                </div>
+            </div>
+
+            <div class="row">
+                <div class="col-xs-6">
+                    <h6>[% l('Circulation') %]</h6>
+                </div>
+                <div class="col-xs-6">
+                    <label>
+                        <input type="checkbox" ng-change="saveDefaults()" ng-model="defaults.attributes.price"/>
+                        [% l('Price') %]
+                    </label>
+                </div>
+            </div>
+
+            <div class="row">
+                <div class="col-xs-6">
+                    <label>
+                        <input type="checkbox" ng-change="saveDefaults()" ng-model="defaults.attributes.circulate"/>
+                        [% l('Circulate?') %]
+                    </label>
+                </div>
+                <div class="col-xs-6">
+                    <label>
+                        <input type="checkbox" ng-change="saveDefaults()" ng-model="defaults.attributes.mint_condition"/>
+                        [% l('Quality') %]
+                    </label>
+                </div>
+            </div>
+
+            <div class="row">
+                <div class="col-xs-6">
+                    <label>
+                        <input type="checkbox" ng-change="saveDefaults()" ng-model="defaults.attributes.circ_lib"/>
+                        [% l('Circulation Library') %]
+                    </label>
+                </div>
+                <div class="col-xs-6">
+                    <label>
+                        <input type="checkbox" ng-change="saveDefaults()" ng-model="defaults.attributes.ref"/>
+                        [% l('Reference?') %]
+                    </label>
+                </div>
+            </div>
+
+            <div class="row">
+                <div class="col-xs-6">
+                    <label>
+                        <input type="checkbox" ng-change="saveDefaults()" ng-model="defaults.attributes.circ_modifier"/>
+                        [% l('Circulation Modifier') %]
+                    </label>
+                </div>
+                <div class="col-xs-6">
+                    <label>
+                        <input type="checkbox" ng-change="saveDefaults()" ng-model="defaults.attributes.cost"/>
+                        [% l('Cost') %]
+                    </label>
+                </div>
+            </div>
+
+            <div class="row">
+                <div class="col-xs-6">
+                    <label>
+                        <input type="checkbox" ng-change="saveDefaults()" ng-model="defaults.attributes.circ_as_type"/>
+                        [% l('Circulate as Type') %]
+                    </label>
+                </div>
+                <div class="col-xs-6">
+                    <label>
+                        <input type="checkbox" ng-change="saveDefaults()" ng-model="defaults.attributes.status"/>
+                        [% l('Status') %]
+                    </label>
+                </div>
+            </div>
+
+            <div class="row">
+                <div class="col-xs-6">
+                    <label>
+                        <input type="checkbox" ng-change="saveDefaults()" ng-model="defaults.attributes.loan_duration"/>
+                        [% l('Loan Duration') %]
+                    </label>
+                </div>
+                <div class="col-xs-6">
+                </div>
+            </div>
+
+            <div class="row">
+                <div class="col-xs-6">
+                    <label>
+                        <input type="checkbox" ng-change="saveDefaults()" ng-model="defaults.attributes.fine_level"/>
+                        [% l('Fine Level') %]
+                    </label>
+                </div>
+                <div class="col-xs-6">
+                    <h6>[% l('Statistical Categories') %]</h6>
+                </div>
+            </div>
+
+            <div class="row">
+                <div class="col-xs-6">
+                    <label>
+                        <input type="checkbox" ng-change="saveDefaults()" ng-model="defaults.attributes.location"/>
+                        [% l('Shelving Location') %]
+                    </label>
+                </div>
+                <div class="col-xs-6">
+                    <label>
+                        <eg-org-selector selected="defaults.statcats.org_filter" noDefault label="[% l('Default Filter Library') %]" disableTest="cant_have_vols"></eg-org-selector>
+                    </label>
+                </div>
+            </div>
+
+            <div class="row">
+                <div class="col-xs-6">
+                    <label>
+                        <input type="checkbox" ng-change="saveDefaults()" ng-model="defaults.attributes.holdable"/>
+                        [% l('Holdable?') %]
+                    </label>
+                </div>
+                <div class="col-xs-6">
+                </div>
+            </div>
+
+            <div class="row">
+                <div class="col-xs-6">
+                    <label>
+                        <input type="checkbox" ng-change="saveDefaults()" ng-model="defaults.attributes.age_protect"/>
+                        [% l('Age-based Hold Protection') %]
+                    </label>
+                </div>
+                <div class="col-xs-6">
+                </div>
+            </div>
+
+        </div>
+
+        <div class="col-md-4">
+            <div class="row">
+                <div class="col-xs-12">
+                    <h4>[% l('Display defaults for Completed Copies tab') %]</h4>
+                </div>
+            </div>
+
+            <div class="row" ng-repeat="col in completedGridControls.columnsProvider().columns">
+                <div class="col-xs-12">
+                    <label>
+                        <input ng-change="completedGridControls.saveConfig()" type="checkbox" ng-model="col.visible"/>
+                        {{col.label}}
+                    </label>
+                </div>
+            </div>
+
+        </div>
+    </div>
+</div>        
diff --git a/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2 b/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2
index 7eefe82..c663610 100644
--- a/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2
@@ -16,19 +16,16 @@
             <div class="col-xs-10">
                 <div class="row">
                     <div class="col-xs-2">
-                        <select class="form-control" ng-model="batch.classification" ng-options="cl.id() as cl.name() for cl in classification_list">
-                        </select>
+                        <select class="form-control" ng-model="batch.classification" ng-options="cl.id() as cl.name() for cl in classification_list"></select>
                     </div>
                     <div class="col-xs-1">
-                        <select class="form-control" ng-model="batch.prefix" ng-options="p.id() as p.label() for p in prefix_list">
-                        </select>
+                        <select class="form-control" ng-model="batch.prefix" ng-options="p.id() as p.label() for p in prefix_list"></select>
                     </div>
                     <div class="col-xs-2">
                         <input class="form-control" type="text" ng-model="batch.label"/>
                     </div>
                     <div class="col-xs-1">
-                        <select class="form-control" ng-model="batch.suffix" ng-options="s.id() as s.label() for s in suffix_list">
-                        </select>
+                        <select class="form-control" ng-model="batch.suffix" ng-options="s.id() as s.label() for s in suffix_list"></select>
                     </div>
                     <div class="col-xs-1"></div>
                     <div class="col-xs-5">
diff --git a/Open-ILS/src/templates/staff/cat/volcopy/t_view.tt2 b/Open-ILS/src/templates/staff/cat/volcopy/t_view.tt2
index cbaa192..0379fad 100644
--- a/Open-ILS/src/templates/staff/cat/volcopy/t_view.tt2
+++ b/Open-ILS/src/templates/staff/cat/volcopy/t_view.tt2
@@ -22,11 +22,9 @@
     <div ng-show="tab == 'templates'">
       <eg-vol-template></eg-vol-template>
     </div>
-<!--
     <div ng-show="tab == 'defaults'">
-      <div ng-include="'[% ctx.base_path %]/staff/cat/volcopy/t_'+tab"></div>
+      <div ng-include="'[% ctx.base_path %]/staff/cat/volcopy/t_defaults'"></div>
     </div>
--->
   </div>
 </div>
 
diff --git a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
index 92d2366..27498fc 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
@@ -482,6 +482,47 @@ function(egCore , $q) {
        ['$scope','$q','$routeParams','$location','$timeout','egCore','egNet','egGridDataProvider','itemSvc',
 function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , egGridDataProvider , itemSvc) {
 
+    $scope.defaults = { // If defaults are not set at all, allow everything
+        attributes : {
+            status : true,
+            loan_duration : true,
+            fine_level : true,
+            cost : true,
+            alerts : true,
+            deposit : true,
+            deposit_amount : true,
+            opac_visible : true,
+            price : true,
+            circulate : true,
+            mint_condition : true,
+            circ_lib : true,
+            ref : true,
+            circ_modifier : true,
+            circ_as_type : true,
+            location : true,
+            holdable : true,
+            age_protect : true
+        }
+    };
+
+    $scope.saveDefaults = function () {
+        egCore.hatch.setItem('cat.copy.defaults', $scope.defaults);
+    }
+
+    $scope.fetchDefaults = function () {
+        egCore.hatch.getItem('cat.copy.defaults').then(function(t) {
+            if (t) {
+                $scope.defaults = t;
+                if (!$scope.batch) $scope.batch = {};
+                $scope.batch.classification = $scope.defaults.classification;
+                $scope.batch.prefix = $scope.defaults.prefix;
+                $scope.batch.suffix = $scope.defaults.suffix;
+                if ($scope.defaults.always_vols) $scope.show_vols = true;
+            }
+        });
+    }
+    $scope.fetchDefaults();
+
     $scope.dirty = false;
 
     $scope.show_vols = true;
@@ -577,7 +618,7 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
         $scope.completed_copies = [];
         $scope.location_orgs = [];
         $scope.location_cache = {};
-        $scope.batch = {};
+        if (!$scope.batch) $scope.batch = {};
 
         $scope.applyBatchCNValues = function () {
             if ($scope.data.tree) {
@@ -630,7 +671,7 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
         ).then(function (data) {
 
             if (data) {
-                if (data.hide_vols) $scope.show_vols = false;
+                if (data.hide_vols && !$scope.defaults.always_vols) $scope.show_vols = false;
                 if (data.hide_copies) $scope.show_copies = false;
 
                 $scope.record_id = data.record_id;
@@ -769,6 +810,38 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
         controller : ['$scope','itemSvc','egCore',
             function ( $scope , itemSvc , egCore ) {
 
+                $scope.defaults = { // If defaults are not set at all, allow everything
+                    attributes : {
+                        status : true,
+                        loan_duration : true,
+                        fine_level : true,
+                        cost : true,
+                        alerts : true,
+                        deposit : true,
+                        deposit_amount : true,
+                        opac_visible : true,
+                        price : true,
+                        circulate : true,
+                        mint_condition : true,
+                        circ_lib : true,
+                        ref : true,
+                        circ_modifier : true,
+                        circ_as_type : true,
+                        location : true,
+                        holdable : true,
+                        age_protect : true
+                    }
+                };
+
+                $scope.fetchDefaults = function () {
+                    egCore.hatch.getItem('cat.copy.defaults').then(function(t) {
+                        if (t) {
+                            $scope.defaults = t;
+                        }
+                    });
+                }
+                $scope.fetchDefaults();
+
                 $scope.dirty = false;
                 $scope.template_controls = true;
 

commit b962aa9a3ee153835a89d6f325198d50011337cf
Author: Mike Rylander <mrylander at gmail.com>
Date:   Fri Aug 21 13:10:41 2015 -0400

    webstaff: Expose columns through grid controls for external column pickers
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/services/grid.js b/Open-ILS/web/js/ui/default/staff/services/grid.js
index b7c1ce3..fbbdb46 100644
--- a/Open-ILS/web/js/ui/default/staff/services/grid.js
+++ b/Open-ILS/web/js/ui/default/staff/services/grid.js
@@ -218,6 +218,18 @@ angular.module('egGridMod',
                 // them up even if the caller doesn't request them.
                 var controls = $scope.gridControls || {};
 
+                controls.columnMap = function() {
+                    var m = {};
+                    angular.forEach(grid.columnsProvider.columns, function (c) {
+                        m[c.name] = c;
+                    });
+                    return m;
+                }
+
+                controls.columnsProvider = function() {
+                    return grid.columnsProvider;
+                }
+
                 // link in the control functions
                 controls.selectedItems = function() {
                     return grid.getSelectedItems()
@@ -287,6 +299,10 @@ angular.module('egGridMod',
                     return grid.offset;
                 }
 
+                controls.saveConfig = function () {
+                    return $scope.saveConfig();
+                }
+
                 grid.dataProvider.refresh = controls.refresh;
                 grid.controls = controls;
             }
@@ -399,6 +415,7 @@ angular.module('egGridMod',
                 });
             }
 
+
             // load the columns configuration (position, sort, width) from
             // eg.grid.<persist-key> and apply the loaded settings to the
             // columns on our columnsProvider
@@ -444,6 +461,7 @@ angular.module('egGridMod',
 
                     grid.columnsProvider.columns = new_cols;
                     grid.compileSort();
+
                 });
             }
 

commit 7a051b66f74cdcb85c551a1b02555d64cfc09957
Author: Mike Rylander <mrylander at gmail.com>
Date:   Thu Aug 20 22:18:02 2015 -0400

    webstaff: Layout improvements and templates
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2 b/Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2
new file mode 100644
index 0000000..509a3da
--- /dev/null
+++ b/Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2
@@ -0,0 +1,333 @@
+<div class="container-fluid">
+<div class="row bg-info">
+    <div class="col-md-1">
+        <h5>[% l('Template') %]</h5>
+    </div>
+    <div class="col-md-2">
+        <eg-basic-combo-box list="template_name_list" selected="template_name"></eg-basic-combo-box>
+    </div>
+    <div class="col-md-1">
+        <button class="btn btn-default " ng-click="applyTemplate(template_name)" type="button">[% l('Apply') %]</button>
+    </div>
+    <div class="col-md-6">
+        <div class="row" ng-show="template_controls">
+            <div class="col-md-4">
+                <div class="btn-group">
+                    <label class="btn btn-default" ng-click="saveTemplate(template_name)">[% l('Save') %]</label>
+                    <label class="btn btn-default" ng-click="deleteTemplate(template_name)">[% l('Delete') %]</label>
+                </div>
+            </div>
+            <div class="col-md-8">
+                <div class="btn-group pull-right">
+                    <label class="btn btn-default" ng-click="importTemplates()">[% l('Import') %]</label>
+                    <label class="btn btn-default" ng-click="exportTemplates()">[% l('Export') %]</label>
+                </div>
+            </div>
+        </div>
+    </div>
+    <div class="col-md-2">
+        <button class="btn btn-default pull-right" ng-click="clearWorking()" type="button">Clear</button>
+    </div>
+</div>
+
+<div class="row pad-vert"></div>
+
+<div class="row bg-info">
+    <div class="col-md-4">
+        <b>[% l('Circulate?') %]</b>
+    </div>
+    <div class="col-md-4">
+        <b>[% l('Status') %]</b>
+    </div>
+    <div class="col-md-4">
+        <b>[% l('Statistical Catagories') %]</b>
+    </div>
+</div>
+
+    <div class="row">
+        <div class="col-md-8">
+            <div class="row">
+                <div class="col-md-6" ng-class="{'bg-success': working.circulate !== undefined}">
+                    <div class="row">
+                        <div class="col-xs-3">
+                            <label>
+                                <input type="radio" ng-model="working.circulate" value="t"/>
+                                [% l('Yes') %]
+                            </label>
+                        </div>
+                        <div class="col-xs-3">
+                            <label>
+                                <input type="radio" ng-model="working.circulate" value="f"/>
+                                [% l('No') %]
+                            </label>
+                        </div>
+                    </div>
+                </div>
+                <div class="col-md-6" ng-class="{'bg-success': working.status !== undefined}">
+                    <select class="form-control"
+                        ng-model="working.status"
+                        ng-options="s.id() as s.name() for s in status_list"
+                    ></select>
+                </div>
+            </div>
+
+            <div class="row pad-vert"></div>
+
+            <div class="row bg-info">
+                <div class="col-md-6">
+                    <b>[% l('Circulation Library') %]</b>
+                </div>
+                <div class="col-md-6">
+                    <b>[% l('Reference?') %]</b>
+                </div>
+            </div>
+
+            <div class="row">
+                <div class="col-md-6" ng-class="{'bg-success': working.circ_lib !== undefined}">
+                    <eg-org-selector selected="working.circ_lib" noDefault label="[% l('(Unset)') %]" disableTest="cant_have_vols"></eg-org-selector>
+                </div>
+                <div class="col-md-6" ng-class="{'bg-success': working.ref !== undefined}">
+                    <div class="row">
+                        <div class="col-xs-3">
+                            <label>
+                                <input type="radio" ng-model="working.ref" value="t"/>
+                                [% l('Yes') %]
+                            </label>
+                        </div>
+                        <div class="col-xs-3">
+                            <label>
+                                <input type="radio" ng-model="working.ref" value="f"/>
+                                [% l('No') %]
+                            </label>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <div class="row pad-vert"></div>
+
+            <div class="row bg-info">
+                <div class="col-md-6">
+                    <b>[% l('Shelving Location') %]</b>
+                </div>
+                <div class="col-md-6">
+                    <b>[% l('OPAC Visible?') %]</b>
+                </div>
+            </div>
+
+            <div class="row">
+                <div class="col-md-6" ng-class="{'bg-success': working.location !== undefined}">
+                    <select class="form-control"
+                        ng-model="working.location"
+                        ng-options="l.id() as l.name() for l in location_list"
+                    ></select>
+                </div>
+                <div class="col-md-6" ng-class="{'bg-success': working.opac_visible !== undefined}">
+                    <div class="row">
+                        <div class="col-xs-3">
+                            <label>
+                                <input type="radio" ng-model="working.opac_visible" value="t"/>
+                                [% l('Yes') %]
+                            </label>
+                        </div>
+                        <div class="col-xs-3">
+                            <label>
+                                <input type="radio" ng-model="working.opac_visible" value="f"/>
+                                [% l('No') %]
+                            </label>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <div class="row pad-vert"></div>
+
+            <div class="row bg-info">
+                <div class="col-md-6">
+                    <b>[% l('Circulation Modifer') %]</b>
+                </div>
+                <div class="col-md-6">
+                    <b>[% l('Price') %]</b>
+                </div>
+            </div>
+
+            <div class="row">
+                <div class="col-md-6" ng-class="{'bg-success': working.circ_modifier !== undefined}">
+                    <select class="form-control"
+                        ng-model="working.circ_modifier"
+                        ng-options="m.code() as m.name() for m in circ_modifier_list"
+                    >
+                        <option value="">[% l('<NONE>') %]</option>
+                    </select>
+                </div>
+                <div class="col-md-6" ng-class="{'bg-success': working.price !== undefined}">
+                    <input class="form-control" ng-model="working.price" type="text"/>
+                </div>
+            </div>
+
+            <div class="row pad-vert"></div>
+
+            <div class="row bg-info">
+                <div class="col-md-6">
+                    <b>[% l('Loan Duration') %]</b>
+                </div>
+                <div class="col-md-6">
+                    <b>[% l('Cost') %]</b>
+                </div>
+            </div>
+
+            <div class="row">
+                <div class="col-md-6" ng-class="{'bg-success': working.loan_duration !== undefined}">
+                    <select class="form-control" ng-model="working.loan_duration">
+                        <option value="1">[% l('Short') %]</option>
+                        <option value="2" selected>[% l('Normal') %]</option>
+                        <option value="3">[% l('Extended') %]</option>
+                    </select>
+                </div>
+                <div class="col-md-6" ng-class="{'bg-success': working.cost !== undefined}">
+                    <input class="form-control" ng-model="working.cost" type="text"/>
+                </div>
+            </div>
+
+            <div class="row pad-vert"></div>
+
+            <div class="row bg-info">
+                <div class="col-md-6">
+                    <b>[% l('Circulate as Type') %]</b>
+                </div>
+                <div class="col-md-6">
+                    <b>[% l('Deposit?') %]</b>
+                </div>
+            </div>
+
+            <div class="row">
+                <div class="col-md-6" ng-class="{'bg-success': working.circ_as_type !== undefined}">
+                    <select class="form-control"
+                        ng-model="working.circ_as_type"
+                        ng-options="t.code() as t.value() for t in circ_type_list"
+                    ></select>
+                </div>
+                <div class="col-md-6" ng-class="{'bg-success': working.deposit !== undefined}">
+                    <div class="row">
+                        <div class="col-xs-3">
+                            <label>
+                                <input type="radio" ng-model="working.deposit" value="t"/>
+                                [% l('Yes') %]
+                            </label>
+                        </div>
+                        <div class="col-xs-3">
+                            <label>
+                                <input type="radio" ng-model="working.deposit" value="f"/>
+                                [% l('No') %]
+                            </label>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <div class="row pad-vert"></div>
+
+            <div class="row bg-info">
+                <div class="col-md-6">
+                    <b>[% l('Holdable?') %]</b>
+                </div>
+                <div class="col-md-6">
+                    <b>[% l('Deposit Amount') %]</b>
+                </div>
+            </div>
+
+            <div class="row">
+                <div class="col-md-6" ng-class="{'bg-success': working.holdable !== undefined}">
+                    <div class="row">
+                        <div class="col-xs-3">
+                            <label>
+                                <input type="radio" ng-model="working.holdable" value="t"/>
+                                [% l('Yes') %]
+                            </label>
+                        </div>
+                        <div class="col-xs-3">
+                            <label>
+                                <input type="radio" ng-model="working.holdable" value="f"/>
+                                [% l('No') %]
+                            </label>
+                        </div>
+                    </div>
+                </div>
+                <div class="col-md-6" ng-class="{'bg-success': working.deposit_amount !== undefined}">
+                    <input class="form-control" ng-model="working.deposit_amount" type="text"/>
+                </div>
+            </div>
+
+            <div class="row pad-vert"></div>
+
+            <div class="row bg-info">
+                <div class="col-md-6">
+                    <b>[% l('Age-based Hold Protection') %]</b>
+                </div>
+                <div class="col-md-6">
+                    <b>[% l('Quality') %]</b>
+                </div>
+            </div>
+
+            <div class="row">
+                <div class="col-md-6" ng-class="{'bg-success': working.age_protect !== undefined}">
+                    <select class="form-control"
+                        ng-model="working.age_protect"
+                        ng-options="a.id() as a.name() for a in age_protect_list"
+                    ></select>
+                </div>
+                <div class="col-md-6" ng-class="{'bg-success': working.mint_condition !== undefined}">
+                    <div class="row">
+                        <div class="col-xs-3">
+                            <label>
+                                <input type="radio" ng-model="working.mint_condition" value="t"/>
+                                [% l('Good') %]
+                            </label>
+                        </div>
+                        <div class="col-xs-3">
+                            <label>
+                                <input type="radio" ng-model="working.mint_condition" value="f"/>
+                                [% l('Damaged') %]
+                            </label>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <div class="row pad-vert"></div>
+
+            <div class="row bg-info">
+                <div class="col-md-6">
+                    <b>[% l('Fine Level') %]</b>
+                </div>
+            </div>
+
+            <div class="row">
+                <div class="col-md-6" ng-class="{'bg-success': working.fine_level !== undefined}">
+                    <select class="form-control" ng-model="working.fine_level">
+                        <option value="1">[% l('Low') %]</option>
+                        <option value="2" selected>[% l('Normal') %]</option>
+                        <option value="3">[% l('High') %]</option>
+                    </select>
+                </div>
+            </div>
+        </div>
+        <div class="col=md-4">
+            statcats<br/>
+            statcats<br/>
+            statcats<br/>
+            statcats<br/>
+            statcats<br/>
+            statcats<br/>
+            statcats<br/>
+            statcats<br/>
+            statcats<br/>
+            statcats<br/>
+            statcats<br/>
+            statcats<br/>
+            statcats<br/>
+            statcats<br/>
+        </div>
+    </div>
+</div>
+</div>
diff --git a/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2 b/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2
index 496d853..7eefe82 100644
--- a/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2
@@ -1,71 +1,69 @@
+<div>
+    <div class="btn-group">
+        <label class="btn btn-default" ng-click="show_vols = !show_vols">
+            <span ng-show="show_vols" style="padding-right: 5px;">[% l('Hide Volume/Copy Details') %]</span>
+            <span ng-hide="show_vols" style="padding-right: 5px;">[% l('Show Volume/Copy Details') %]</span>
+        </label>
+        <label class="btn btn-default" ng-click="show_copies = !show_copies">
+            <span ng-show="show_copies" style="padding-right: 5px;">[% l('Hide Copy Attributes') %]</span>
+            <span ng-hide="show_copies" style="padding-right: 5px;">[% l('Show Copy Attributes') %]</span>
+        </label>
+    </div>
 
-<style> input[type=number] { width: 50px } </style>
-<style> select { width: 80px } </style>
-
-<accordion close-others="false">
-    <accordion-group heading="Volume/Copy Details" is-open="show_vols">
-
-        <div class="container-fluid">
-            <div class="row bg-info">
-                <div class="col-xs-2"><h4 class="center-block">[% l('Batch Apply') %]</h4></div>
-                <div class="col-xs-10">
-                    <div class="container-fluid">
-                        <div class="row">
-                            <div class="col-xs-1">
-                                <select ng-model="batch.classification" ng-options="cl.id() as cl.name() for cl in classification_list">
-                                    <option value="">[% l('(Unset)') %]</option>
-                                </select>
-                            </div>
-                            <div class="col-xs-1">
-                                <select ng-model="batch.prefix" ng-options="p.id() as p.label() for p in prefix_list">
-                                    <option value="">[% l('(Unset)') %]</option>
-                                </select>
-                            </div>
-                            <div class="col-xs-3">
-                                <input type="text" ng-model="batch.label"/>
-                            </div>
-                            <div class="col-xs-1">
-                                <select ng-model="batch.suffix" ng-options="s.id() as s.label() for s in suffix_list">
-                                    <option value="">[% l('(Unset)') %]</option>
-                                </select>
-                            </div>
-                            <div class="col-xs-1"></div>
-                            <div class="col-xs-5">
-                                <button class="btn btn-default center-block" ng-click="applyBatchCNValues()" type="button">Apply</button>
-                            </div>
-                        </div>
+    <div class="container-fluid pad-vert" ng-show="show_vols">
+        <div class="row bg-info">
+            <div class="col-xs-2"><h4 class="center-block">[% l('Batch Apply') %]</h4></div>
+            <div class="col-xs-10">
+                <div class="row">
+                    <div class="col-xs-2">
+                        <select class="form-control" ng-model="batch.classification" ng-options="cl.id() as cl.name() for cl in classification_list">
+                        </select>
+                    </div>
+                    <div class="col-xs-1">
+                        <select class="form-control" ng-model="batch.prefix" ng-options="p.id() as p.label() for p in prefix_list">
+                        </select>
+                    </div>
+                    <div class="col-xs-2">
+                        <input class="form-control" type="text" ng-model="batch.label"/>
+                    </div>
+                    <div class="col-xs-1">
+                        <select class="form-control" ng-model="batch.suffix" ng-options="s.id() as s.label() for s in suffix_list">
+                        </select>
+                    </div>
+                    <div class="col-xs-1"></div>
+                    <div class="col-xs-5">
+                        <button class="btn btn-default center-block" ng-click="applyBatchCNValues()" type="button">Apply</button>
                     </div>
                 </div>
-            </div> <!-- row -->
-            <div class="row">
-                <div class="col-xs-1"><h5>[% l('Library') %]</h5></div>
-                <div class="col-xs-1"><h5>[% l('Volumes') %]</h5></div>
-                <div class="col-xs-10">
-                    <div class="container-fluid">
+            </div>
+        </div>
+        <div class="row pad-vert">
+            <div class="col-xs-1"><b>[% l('Library') %]</b></div>
+            <div class="col-xs-1"><b>[% l('Volumes') %]</b></div>
+            <div class="col-xs-10">
+                <div class="row">
+                    <div class="col-xs-2"><b>[% l('Classification') %]</b></div>
+                    <div class="col-xs-1"><b>[% l('Prefix') %]</b></div>
+                    <div class="col-xs-2"><b>[% l('Call Number') %]</b></div>
+                    <div class="col-xs-1"><b>[% l('Suffix') %]</b></div>
+                    <div class="col-xs-1"><b>[% l('Copies') %]</b></div>
+                    <div class="col-xs-5">
                         <div class="row">
-                            <div class="col-xs-1"><h5>[% l('Classification') %]</h5></div>
-                            <div class="col-xs-1"><h5>[% l('Prefix') %]</h5></div>
-                            <div class="col-xs-3"><h5>[% l('Call Number') %]</h5></div>
-                            <div class="col-xs-1"><h5>[% l('Suffix') %]</h5></div>
-                            <div class="col-xs-1"><h5>[% l('Copies') %]</h5></div>
-                            <div class="col-xs-5">
-                                <div class="container-fluid">
-                                    <div class="row">
-                                        <div class="col-xs-6"><h5>[% l('Barcode') %]</h5></div>
-                                        <div class="col-xs-3"><h5>[% l('Copy #') %]</h5></div>
-                                        <div class="col-xs-3"><h5>[% l('Part') %]</h5></div>
-                                    </div>
-                                </div>
-                            </div>
+                            <div class="col-xs-5"><b>[% l('Barcode') %]</b></div>
+                            <div class="col-xs-3"><b>[% l('Copy #') %]</b></div>
+                            <div class="col-xs-4"><b>[% l('Part') %]</b></div>
                         </div>
                     </div>
                 </div>
-            </div> <!-- row -->
-            <eg-vol-edit ng-repeat="(lib,callnumbers) in data.tree" record="{{record.id()}}" lib="{{lib}}" allcopies="data.copies" struct="data.tree[lib]"></eg-vol-edit>
-        </div> <!-- container -->
+            </div>
+        </div> <!-- row -->
+        <eg-vol-edit ng-repeat="(lib,callnumbers) in data.tree" record="record.id()" lib="{{lib}}" allcopies="data.copies" struct="data.tree[lib]"></eg-vol-edit>
+    </div>
+
+</div>
+<div class="pad-vert">
 
-    </accordion-group>
-    <accordion-group heading="Copy Attributes" is-open="show_copies">
+    <div class="pad-vert" ng-show="show_copies">
 
         <ul ng-model="copytab" class="nav nav-tabs">
           <li ng-class="{active : copytab == 'working'}">
@@ -76,399 +74,74 @@
           </li>
         </ul>
 
-<div class="tab-content">
-  <div class="tab-pane active">
-    <div ng-show="copytab == 'working'">
-
-    <div class="container-fluid"> <!-- working copy editor -->
-        <div class="row">
-            <div class="col-lg-4">
-
-                <eg-grid
-                  id-field="id"
-                  idl-class="acp"
-                  features="startSelected,-pagination,-actions,-picker,-index"
-                  items-provider="workingGridDataProvider"
-                  grid-controls="workingGridControls"
-                  persist-key="cat.volcopy.copies">
-
-                  <eg-grid-menu-item handler="workingToComplete"
-                   label="[% l('Store Selected') %]"></eg-grid-menu-item>
-
-                
-                  <eg-grid-field label="[% l('Barcode') %]"     path='barcode' visible></eg-grid-field>
-                  <eg-grid-field label="[% l('Created') %]"     path="create_date" visible></eg-grid-field>
-                  <eg-grid-field label="[% l('Activated') %]"   path="active_date" visible></eg-grid-field>
-                  <eg-grid-field label="[% l('Call Number') %]" path="call_number.label" visible></eg-grid-field>
-                
-                </eg-grid>
-
-            </div>
-            <div class="col-lg-8">
-                <div class="container-fluid">
-                    <div class="row">
-
-                        <div class="col-md-4">
-                            <div class="container-fluid"> <!-- first column -->
-
-                                <div class="row">
-                                    <div class="col-md-12">
-                                        <h5>[% l('Circulate?') %]</h5>
-                                    </div>
-                                </div>
-                                <div class="row">
-                                    <div class="col-md-12">
-                                        <div class="container-fluid"> <!-- circulate? -->
-                                            <div class="row">
-                                                <div class="col-xs-6">
-                                                    <label>
-                                                        <input type="radio" ng-model="working.circulate" value="t"/>
-                                                        [% l('Yes') %]
-                                                    </label>
-                                                </div>
-                                                <div class="col-xs-6">
-                                                    <label>
-                                                        <input type="radio" ng-model="working.circulate" value="f"/>
-                                                        [% l('No') %]
-                                                    </label>
-                                                </div>
-                                            </div>
-                                        </div>
-                                    </div>
-                                </div>
-
-                                <div class="row">
-                                    <div class="col-md-12">
-                                        <h5>[% l('Circulation Library') %]</h5>
-                                    </div>
-                                </div>
-                                <div class="row">
-                                    <div class="col-md-12">
-                                        <eg-org-selector selected="working.circ_lib" disableTest="cant_have_vols"></eg-org-selector>
-                                    </div>
-                                </div>
-
-                                <div class="row">
-                                    <div class="col-md-12">
-                                        <h5>[% l('Shelving Location') %]</h5>
-                                    </div>
-                                </div>
-                                <div class="row">
-                                    <div class="col-md-12">
-                                        <select
-                                            ng-model="working.location"
-                                            ng-options="l.name() for l in location_list track by idTracker(l)"
-                                        ></select>
-                                    </div>
-                                </div>
-
-                                <div class="row">
-                                    <div class="col-md-12">
-                                        <h5>[% l('Circulation Modifier') %]</h5>
-                                    </div>
-                                </div>
-                                <div class="row">
-                                    <div class="col-md-12">
-                                        <select
-                                            ng-model="working.circ_modifier"
-                                            ng-options="m.name() for m in circ_mod_list track by tracker(m,'code')"
-                                        ></select>
-                                    </div>
-                                </div>
-
-                                <div class="row">
-                                    <div class="col-md-12">
-                                        <h5>[% l('Circulate as Type') %]</h5>
-                                    </div>
-                                </div>
-                                <div class="row">
-                                    <div class="col-md-12">
-                                        <select
-                                            ng-model="working.circ_as_type"
-                                            ng-options="t.value() for t in circ_type_list track by tracker(t,'code')"
-                                        ></select>
-                                    </div>
-                                </div>
-
-                                <div class="row">
-                                    <div class="col-md-12">
-                                        <h5>[% l('Holdable?') %]</h5>
-                                    </div>
-                                </div>
-                                <div class="row">
-                                    <div class="col-md-12">
-                                        <div class="container-fluid">
-                                            <div class="row">
-                                                <div class="col-xs-6">
-                                                    <label>
-                                                        <input type="radio" ng-model="working.holdable" value="t"/>
-                                                        [% l('Yes') %]
-                                                    </label>
-                                                </div>
-                                                <div class="col-xs-6">
-                                                    <label>
-                                                        <input type="radio" ng-model="working.holdable" value="f"/>
-                                                        [% l('No') %]
-                                                    </label>
-                                                </div>
-                                            </div>
-                                        </div>
-                                    </div>
-                                </div>
-
-                                <div class="row">
-                                    <div class="col-md-12">
-                                        <h5>[% l('Loan Duration') %]</h5>
-                                    </div>
-                                </div>
-                                <div class="row">
-                                    <div class="col-md-12">
-                                        <select ng-model="working.loan_duration">
-                                            <option value="1">[% l('Short') %]</option>
-                                            <option value="2" selected>[% l('Normal') %]</option>
-                                            <option value="3">[% l('Extended') %]</option>
-                                        </select>
-                                    </div>
-                                </div>
-
-                                <div class="row">
-                                    <div class="col-md-12">
-                                        <h5>[% l('Fine Level') %]</h5>
-                                    </div>
-                                </div>
-                                <div class="row">
-                                    <div class="col-md-12">
-                                        <select ng-model="working.fine_level">
-                                            <option value="1">[% l('Low') %]</option>
-                                            <option value="2" selected>[% l('Normal') %]</option>
-                                            <option value="3">[% l('High') %]</option>
-                                        </select>
-                                    </div>
-                                </div>
-
-                                <div class="row">
-                                    <div class="col-md-12">
-                                        <h5>[% l('Age-based Hold Protection') %]</h5>
-                                    </div>
-                                </div>
-                                <div class="row">
-                                    <div class="col-md-12">
-                                        <select
-                                            ng-model="working.age_protect"
-                                            ng-options="a.name() for a in age_protect_list track by idTracker(a)"
-                                        ></select>
-                                    </div>
-                                </div>
-
-                            </div> <!-- first column -->
-                        </div>
-
-                        <div class="col-md-4">
-                            <div class="container-fluid"> <!-- second column -->
-
-                                <div class="row">
-                                    <div class="col-md-12">
-                                        <h5>[% l('Status') %]</h5>
-                                    </div>
-                                </div>
-                                <div class="row">
-                                    <div class="col-md-12">
-                                        <select
-                                            ng-model="working.status"
-                                            ng-options="s.name() for s in status_list track by idTracker(s)"
-                                        ></select>
-                                    </div>
-                                </div>
-
-                                <div class="row">
-                                    <div class="col-md-12">
-                                        <h5>[% l('Reference?') %]</h5>
-                                    </div>
-                                </div>
-                                <div class="row">
-                                    <div class="col-md-12">
-                                        <div class="container-fluid">
-                                            <div class="row">
-                                                <div class="col-xs-6">
-                                                    <label>
-                                                        <input type="radio" ng-model="working.ref" value="t"/>
-                                                        [% l('Yes') %]
-                                                    </label>
-                                                </div>
-                                                <div class="col-xs-6">
-                                                    <label>
-                                                        <input type="radio" ng-model="working.ref" value="f"/>
-                                                        [% l('No') %]
-                                                    </label>
-                                                </div>
-                                            </div>
-                                        </div>
-                                    </div>
-                                </div>
-
-                                <div class="row">
-                                    <div class="col-md-12">
-                                        <h5>[% l('OPAC Visible?') %]</h5>
-                                    </div>
-                                </div>
-                                <div class="row">
-                                    <div class="col-md-12">
-                                        <div class="container-fluid">
-                                            <div class="row">
-                                                <div class="col-xs-6">
-                                                    <label>
-                                                        <input type="radio" ng-model="working.opac_visible" value="t"/>
-                                                        [% l('Yes') %]
-                                                    </label>
-                                                </div>
-                                                <div class="col-xs-6">
-                                                    <label>
-                                                        <input type="radio" ng-model="working.opac_visible" value="f"/>
-                                                        [% l('No') %]
-                                                    </label>
-                                                </div>
-                                            </div>
-                                        </div>
-                                    </div>
-                                </div>
-
-                                <div class="row">
-                                    <div class="col-md-12">
-                                        <h5>[% l('Price') %]</h5>
-                                    </div>
-                                </div>
-                                <div class="row">
-                                    <div class="col-md-12">
-                                        <input ng-model="working.price" type="text"/>
-                                    </div>
-                                </div>
-
-                                <div class="row">
-                                    <div class="col-md-12">
-                                        <h5>[% l('Cost') %]</h5>
-                                    </div>
-                                </div>
-                                <div class="row">
-                                    <div class="col-md-12">
-                                        <input ng-model="working.cost" type="text"/>
-                                    </div>
-                                </div>
-
-                                <div class="row">
-                                    <div class="col-md-12">
-                                        <h5>[% l('Deposit?') %]</h5>
-                                    </div>
-                                </div>
-                                <div class="row">
-                                    <div class="col-md-12">
-                                        <div class="container-fluid">
-                                            <div class="row">
-                                                <div class="col-xs-6">
-                                                    <label>
-                                                        <input type="radio" ng-model="working.deposit" value="t"/>
-                                                        [% l('Yes') %]
-                                                    </label>
-                                                </div>
-                                                <div class="col-xs-6">
-                                                    <label>
-                                                        <input type="radio" ng-model="working.deposit" value="f"/>
-                                                        [% l('No') %]
-                                                    </label>
-                                                </div>
-                                            </div>
-                                        </div>
-                                    </div>
-                                </div>
-
-                                <div class="row">
-                                    <div class="col-md-12">
-                                        <h5>[% l('Deposit Amount') %]</h5>
-                                    </div>
-                                </div>
-                                <div class="row">
-                                    <div class="col-md-12">
-                                        <input ng-model="working.deposit_amount" type="text"/>
-                                    </div>
-                                </div>
-
-                                <div class="row">
-                                    <div class="col-md-12">
-                                        <h5>[% l('Quality') %]</h5>
-                                    </div>
-                                </div>
-                                <div class="row">
-                                    <div class="col-md-12">
-                                        <div class="container-fluid">
-                                            <div class="row">
-                                                <div class="col-xs-6">
-                                                    <label>
-                                                        <input type="radio" ng-model="working.mint_condition" value="t"/>
-                                                        [% l('Good') %]
-                                                    </label>
-                                                </div>
-                                                <div class="col-xs-6">
-                                                    <label>
-                                                        <input type="radio" ng-model="working.mint_condition" value="f"/>
-                                                        [% l('Damaged') %]
-                                                    </label>
-                                                </div>
-                                            </div>
-                                        </div>
-                                    </div>
-                                </div>
-
-                            </div>
-                        </div>
-
-                        <div class="col-md-4">
-                            <div class="container-fluid"> <!-- third column -->
-                                <div class="row">
-                                    <div class="col-md-12">
-                                        <h5>[% l('Statistical Catagories') %]</h5>
-                                    </div>
-                                </div>
-                            </div>
-                        </div>
-
+        <div class="tab-content">
+          <div class="tab-pane active">
+            <div ng-show="copytab == 'working'">
+        
+            <div class="container-fluid"> <!-- working copy editor -->
+                <div class="row">
+                    <div class="col-lg-4">
+        
+                        <eg-grid
+                          id-field="id"
+                          idl-class="acp"
+                          features="startSelected,-pagination,-actions,-picker,-index"
+                          items-provider="workingGridDataProvider"
+                          grid-controls="workingGridControls"
+                          persist-key="cat.volcopy.copies">
+        
+                          <eg-grid-menu-item handler="workingToComplete"
+                           label="[% l('Store Selected') %]"></eg-grid-menu-item>
+        
+                        
+                          <eg-grid-field label="[% l('Barcode') %]"     path='barcode' visible></eg-grid-field>
+                          <eg-grid-field label="[% l('Created') %]"     path="create_date" visible></eg-grid-field>
+                          <eg-grid-field label="[% l('Activated') %]"   path="active_date" visible></eg-grid-field>
+                          <eg-grid-field label="[% l('Call Number') %]" path="call_number.label" visible></eg-grid-field>
+                        
+                        </eg-grid>
+        
+                    </div>
+        
+                    <div class="col-lg-8 give-cell-border">
+                        <div ng-include="'[% ctx.base_path %]/staff/cat/volcopy/t_attr_edit'"></div>
                     </div>
                 </div>
             </div>
+        
+            </div>
+            <div ng-show="copytab == 'complete'">
+               <eg-grid
+                 id-field="id"
+                 idl-class="acp"
+                 features="-pagination,-actions,-picker,-index"
+                 items-provider="completedGridDataProvider"
+                 grid-controls="completedGridControls"
+                 persist-key="cat.volcopy.copies.complete">
+               
+                 <eg-grid-menu-item handler="completeToWorking"
+                  label="[% l('Edit Selected') %]"></eg-grid-menu-item>
+        
+                 <eg-grid-menu-item handler="saveAndExit"
+                  label="[% l('Save & Exit') %]"></eg-grid-menu-item>
+        
+                 <eg-grid-field label="[% l('Barcode') %]"     path='barcode' visible></eg-grid-field>
+                 <eg-grid-field label="[% l('Created') %]"     path="create_date" visible></eg-grid-field>
+                 <eg-grid-field label="[% l('Activated') %]"   path="active_date" visible></eg-grid-field>
+                 <eg-grid-field label="[% l('Call Number') %]" path="call_number.label" visible></eg-grid-field>
+                 <eg-grid-field label="[% l('Circ Library') %]" flesher="orgById" path="circ_lib.name" visible></eg-grid-field>
+                 <eg-grid-field label="[% l('Shelving Location') %]" flesher="locationById" path="location.name" visible></eg-grid-field>
+                 <eg-grid-field label="[% l('Circ Modifier') %]" path="circ_modifier" visible></eg-grid-field>
+                 <eg-grid-field label="[% l('Circulate?') %]"  path="circulate" visible></eg-grid-field>
+                 <eg-grid-field label="[% l('Holdable?') %]"   path="circulate" visible></eg-grid-field>
+                 <eg-grid-field label="[% l('Reference?') %]"  path="ref" visible></eg-grid-field>
+                 <eg-grid-field label="[% l('Status') %]"      flesher="statusById" path="status.name" visible></eg-grid-field>
+                 <eg-grid-field label="[% l('OPAC Visible') %]" path="opac_visible" visible></eg-grid-field>
+               
+               </eg-grid>
+            </div>
+          </div>
         </div>
-    </div>
-
-    </div>
-    <div ng-show="copytab == 'complete'">
-       <eg-grid
-         id-field="id"
-         idl-class="acp"
-         features="-pagination,-actions,-picker,-index"
-         items-provider="completedGridDataProvider"
-         grid-controls="completedGridControls"
-         persist-key="cat.volcopy.copies.complete">
-       
-         <eg-grid-menu-item handler="completeToWorking"
-          label="[% l('Edit Selected') %]"></eg-grid-menu-item>
-
-         <eg-grid-menu-item handler="saveAndExit"
-          label="[% l('Save & Exit') %]"></eg-grid-menu-item>
 
-         <eg-grid-field label="[% l('Barcode') %]"     path='barcode' visible></eg-grid-field>
-         <eg-grid-field label="[% l('Created') %]"     path="create_date" visible></eg-grid-field>
-         <eg-grid-field label="[% l('Activated') %]"   path="active_date" visible></eg-grid-field>
-         <eg-grid-field label="[% l('Call Number') %]" path="call_number.label" visible></eg-grid-field>
-         <eg-grid-field label="[% l('Circ Library') %]" flesher="orgById" path="circ_lib.name" visible></eg-grid-field>
-         <eg-grid-field label="[% l('Shelving Location') %]" flesher="locationById" path="location.name" visible></eg-grid-field>
-         <eg-grid-field label="[% l('Circ Modifier') %]" path="circ_modifier" visible></eg-grid-field>
-         <eg-grid-field label="[% l('Circulate?') %]"  path="circulate" visible></eg-grid-field>
-         <eg-grid-field label="[% l('Holdable?') %]"   path="circulate" visible></eg-grid-field>
-         <eg-grid-field label="[% l('Reference?') %]"  path="ref" visible></eg-grid-field>
-         <eg-grid-field label="[% l('Status') %]"      flesher="statusById" path="status.name" visible></eg-grid-field>
-         <eg-grid-field label="[% l('OPAC Visible') %]" path="opac_visible" visible></eg-grid-field>
-       
-       </eg-grid>
     </div>
-  </div>
 </div>
-
-    </accordion-group>
-</accordion>
diff --git a/Open-ILS/src/templates/staff/cat/volcopy/t_view.tt2 b/Open-ILS/src/templates/staff/cat/volcopy/t_view.tt2
index d31c26f..cbaa192 100644
--- a/Open-ILS/src/templates/staff/cat/volcopy/t_view.tt2
+++ b/Open-ILS/src/templates/staff/cat/volcopy/t_view.tt2
@@ -1,19 +1,16 @@
 <eg-record-summary record-id="record_id" record="summaryRecord"></eg-record-summary>
 
 <!-- tabbed copy data view -->
-<h1>[% l('Volume/Copy Editor') %]</h1>
-
-<div class="pad-vert"></div>
 
 <ul class="nav nav-tabs">
   <li ng-class="{active : tab == 'edit'}">
-    <a ng-click="set_volcopy_tab('edit')" >[% l('Edit') %]</a>
+    <a ng-click="tab = 'edit'" >[% l('Edit') %]</a>
   </li>
   <li ng-class="{active : tab == 'templates'}">
-    <a ng-click="set_volcopy_tab('templates')" >[% l('Templates') %]</a>
+    <a ng-click="tab = 'templates'" >[% l('Copy Templates') %]</a>
   </li>
   <li ng-class="{active : tab == 'defaults'}">
-    <a ng-click="set_volcopy_tab('defaults')" >[% l('defaults') %]</a>
+    <a ng-click="tab = 'defaults'" >[% l('Defaults') %]</a>
   </li>
 </ul>
 
@@ -22,10 +19,10 @@
     <div ng-show="tab == 'edit'">
       <div ng-include="'[% ctx.base_path %]/staff/cat/volcopy/t_edit'"></div>
     </div>
-<!--
     <div ng-show="tab == 'templates'">
-      <div ng-include="'[% ctx.base_path %]/staff/cat/volcopy/t_'+tab"></div>
+      <eg-vol-template></eg-vol-template>
     </div>
+<!--
     <div ng-show="tab == 'defaults'">
       <div ng-include="'[% ctx.base_path %]/staff/cat/volcopy/t_'+tab"></div>
     </div>
diff --git a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
index cbad7dc..92d2366 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
@@ -179,16 +179,24 @@ function(egCore , $q) {
         replace: true,
         template:
             '<div class="row">'+
-                '<div class="col-xs-6"><input type="text" ng-model="barcode" ng-change="updateBarcode()"/></div>'+
-                '<div class="col-xs-2"><input type="number" ng-model="copy_number" ng-change="updateCopyNo()"/></div>'+
+                '<div class="col-xs-5">'+
+                    '<input id="{{callNumber.id()}}.{{copy.id()}}"'+
+                    ' eg-enter="nextBarcode()" class="form-control"'+
+                    ' type="text" ng-model="barcode" ng-change="updateBarcode()"/>'+
+                '</div>'+
+                '<div class="col-xs-3"><input class="form-control" type="number" ng-model="copy_number" ng-change="updateCopyNo()"/></div>'+
                 '<div class="col-xs-4"><eg-basic-combo-box list="parts" selected="part"></eg-basic-combo-box></div>'+
             '</div>',
 
-        scope: { copy: "=", callNumber: "=" },
+        scope: { copy: "=", callNumber: "=", index: "@" },
         controller : ['$scope','itemSvc',
             function ( $scope , itemSvc ) {
                 $scope.new_part_id = 0;
 
+                $scope.nextBarcode = function (i) {
+                    $scope.$parent.focusNextBarcode($scope.copy.id());
+                }
+
                 $scope.updateBarcode = function () { $scope.copy.barcode($scope.barcode); $scope.copy.ischanged(1); };
                 $scope.updateCopyNo = function () { $scope.copy.copy_number($scope.copy_number); $scope.copy.ischanged(1); };
                 $scope.updatePart = function () {
@@ -237,21 +245,19 @@ function(egCore , $q) {
         transclude: true,
         template:
             '<div class="row">'+
-                '<div class="col-xs-1">'+
-                    '<select ng-model="classification" ng-options="cl.name() for cl in classification_list track by idTracker(cl)"/>'+
+                '<div class="col-xs-2">'+
+                    '<select class="form-control" ng-model="classification" ng-options="cl.name() for cl in classification_list track by idTracker(cl)"/>'+
                 '</div>'+
                 '<div class="col-xs-1">'+
-                    '<select ng-model="prefix" ng-change="updatePrefix()" ng-options="p.label() for p in prefix_list track by idTracker(p)"/>'+
+                    '<select class="form-control" ng-model="prefix" ng-change="updatePrefix()" ng-options="p.label() for p in prefix_list track by idTracker(p)"/>'+
                 '</div>'+
-                '<div class="col-xs-3"><input type="text" ng-change="updateLabel()" ng-model="label"/></div>'+
+                '<div class="col-xs-2"><input class="form-control" type="text" ng-change="updateLabel()" ng-model="label"/></div>'+
                 '<div class="col-xs-1">'+
-                    '<select ng-model="suffix" ng-change="updateSuffix()" ng-options="s.label() for s in suffix_list track by idTracker(s)"/>'+
+                    '<select class="form-control" ng-model="suffix" ng-change="updateSuffix()" ng-options="s.label() for s in suffix_list track by idTracker(s)"/>'+
                 '</div>'+
-                '<div class="col-xs-1"><input type="number" ng-model="copy_count" min="{{orig_copy_count}}" ng-change="changeCPCount()"></div>'+
+                '<div class="col-xs-1"><input class="form-control" type="number" ng-model="copy_count" min="{{orig_copy_count}}" ng-change="changeCPCount()"></div>'+
                 '<div class="col-xs-5">'+
-                    '<div class="container-fluid">'+
-                        '<eg-vol-copy-edit ng-repeat="cp in copies track by idTracker(cp)" copy="cp" call-number="callNumber"></eg-vol-copy-edit>'+
-                    '</div>'+
+                    '<eg-vol-copy-edit ng-repeat="cp in copies track by idTracker(cp)" copy="cp" call-number="callNumber"></eg-vol-copy-edit>'+
                 '</div>'+
             '</div>',
 
@@ -263,6 +269,28 @@ function(egCore , $q) {
 
                 $scope.idTracker = function (x) { if (x) return x.id() };
 
+                // XXX $() is not working! arg
+                $scope.focusNextBarcode = function (i) {
+                    var n;
+                    var yep = false;
+                    angular.forEach($scope.copies, function (cp) {
+                        if (n) return;
+
+                        if (cp.id() == i) {
+                            yep = true;
+                            return;
+                        }
+
+                        if (yep) n = cp.id();
+                    });
+
+                    if (n) {
+                        var next = '#' + $scope.callNumber.id() + '.' + n;
+                        var el = $(next).get(0);
+                        if (el) el.focus()
+                    }
+                }
+
                 $scope.suffix_list = [];
                 itemSvc.get_suffixes($scope.callNumber.owning_lib()).then(function(list){
                     $scope.suffix_list = list;
@@ -380,11 +408,9 @@ function(egCore , $q) {
         template:
             '<div class="row">'+
                 '<div class="col-xs-1"><eg-org-selector selected="owning_lib" disableTest="cant_have_vols"></eg-org-selector></div>'+
-                '<div class="col-xs-1"><input type="number" min="{{orig_cn_count}}" ng-model="cn_count" ng-change="changeCNCount()"/></div>'+
+                '<div class="col-xs-1"><input class="form-control" type="number" min="{{orig_cn_count}}" ng-model="cn_count" ng-change="changeCNCount()"/></div>'+
                 '<div class="col-xs-10">'+
-                    '<div class="container-fluid">'+
-                        '<eg-vol-row ng-repeat="(cn,copies) in struct track by cn" copies="copies" allcopies="allcopies"></eg-vol-row>'+
-                    '</div>'+
+                    '<eg-vol-row ng-repeat="(cn,copies) in struct track by cn" copies="copies" allcopies="allcopies"></eg-vol-row>'+
                 '</div>'+
             '</div>',
 
@@ -456,6 +482,8 @@ function(egCore , $q) {
        ['$scope','$q','$routeParams','$location','$timeout','egCore','egNet','egGridDataProvider','itemSvc',
 function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , egGridDataProvider , itemSvc) {
 
+    $scope.dirty = false;
+
     $scope.show_vols = true;
     $scope.show_copies = true;
 
@@ -492,26 +520,53 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
     createSimpleUpdateWatcher = function (field) {
         $scope.$watch('working.' + field, function () {
             var newval = $scope.working[field];
-            if (angular.isObject(newval)) { // we'll use the pkey
-                if (newval.id) newval = newval.id();
-                else if (newval.code) newval = newval.code();
-            }
 
-            if ($scope.workingGridControls && $scope.workingGridControls.selectedItems) {
-                angular.forEach(
-                    $scope.workingGridControls.selectedItems(),
-                    function (cp) { cp[field](newval); cp.ischanged(1); }
-                );
+            if (typeof newval != 'undefined') {
+                if (angular.isObject(newval)) { // we'll use the pkey
+                    if (newval.id) newval = newval.id();
+                    else if (newval.code) newval = newval.code();
+                }
+
+                if (newval == "") {
+                    $scope.working[field] = undefined;
+                    newval = null;
+                }
+
+                if ($scope.workingGridControls && $scope.workingGridControls.selectedItems) {
+                    angular.forEach(
+                        $scope.workingGridControls.selectedItems(),
+                        function (cp) { cp[field](newval); cp.ischanged(1); }
+                    );
+                }
             }
         });
     }
 
-    $timeout(function(){
+    $scope.applyTemplate = function (n) {
+        angular.forEach($scope.templates[n], function (v,k) {
+            $scope.working[k] = angular.copy(v);
+        });
+    }
 
     var dataKey = $routeParams.dataKey;
     console.debug('dataKey: ' + dataKey);
 
     if (dataKey && dataKey.length > 0) {
+
+        $scope.templates = {};
+        $scope.template_name = '';
+        $scope.template_name_list = [];
+
+        $scope.fetchTemplates = function () {
+            egCore.hatch.getItem('cat.copy.templates').then(function(t) {
+                if (t) {
+                    $scope.templates = t;
+                    $scope.template_name_list = Object.keys(t);
+                }
+            });
+        }
+        $scope.fetchTemplates();
+ 
         $scope.working = {};
 
         $scope.copytab = 'working';
@@ -543,6 +598,13 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
             }
         }
 
+        $scope.clearWorking = function () {
+            angular.forEach($scope.working, function (v,k,o) {
+                if (typeof v != 'undefined')
+                    $scope.working[k] = undefined;
+            });
+        }
+
         $scope.completedGridDataProvider = egGridDataProvider.instance({
             get : function(offset, count) {
                 //return provider.arrayNotifier(itemSvc.copies, offset, count);
@@ -567,30 +629,32 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
             dataKey, 'edit-these-copies'
         ).then(function (data) {
 
-            if (data.hide_vols) $scope.show_vols = false;
-            if (data.hide_copies) $scope.show_copies = false;
+            if (data) {
+                if (data.hide_vols) $scope.show_vols = false;
+                if (data.hide_copies) $scope.show_copies = false;
 
-            $scope.record_id = data.record_id;
+                $scope.record_id = data.record_id;
 
-            if (data.copies && data.copies.length)
-                return itemSvc.fetchIds(data.copies);
+                if (data.copies && data.copies.length)
+                    return itemSvc.fetchIds(data.copies);
 
-            if (data.raw && data.raw.length) {
+                if (data.raw && data.raw.length) {
 
-                /* data.raw must be an array of copies with (at least)
-                 * the call number fleshed on each.  For new copies
-                 * create from whole cloth, the id for each should
-                 * probably be negative and isnew() should return true.
-                 * Each /distinct/ call number must have a distinct id
-                 * as well, probably negative also if they're new. Clear?
-                 */
+                    /* data.raw must be an array of copies with (at least)
+                     * the call number fleshed on each.  For new copies
+                     * create from whole cloth, the id for each should
+                     * probably be negative and isnew() should return true.
+                     * Each /distinct/ call number must have a distinct id
+                     * as well, probably negative also if they're new. Clear?
+                     */
 
-                angular.forEach(
-                    data.raw,
-                    function (cp) { itemSvc.addCopy(cp) }
-                );
+                    angular.forEach(
+                        data.raw,
+                        function (cp) { itemSvc.addCopy(cp) }
+                    );
 
-                return itemSvc.copies;
+                    return itemSvc.copies;
+                }
             }
 
         }).then( function() {
@@ -662,9 +726,9 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
         });
         createSimpleUpdateWatcher('status');
 
-        $scope.circ_mod_list = [];
+        $scope.circ_modifier_list = [];
         itemSvc.get_circ_mods().then(function(list){
-            $scope.circ_mod_list = list;
+            $scope.circ_modifier_list = list;
         });
         createSimpleUpdateWatcher('circ_modifier');
 
@@ -680,6 +744,7 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
         });
         createSimpleUpdateWatcher('age_protect');
 
+        createSimpleUpdateWatcher('circ_lib');
         createSimpleUpdateWatcher('circulate');
         createSimpleUpdateWatcher('holdable');
         createSimpleUpdateWatcher('fine_level');
@@ -693,8 +758,159 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
 
     }
 
-    });
-
 }])
 
+.directive("egVolTemplate", function () {
+    return {
+        restrict: 'E',
+        replace: true,
+        template: '<div ng-include="'+"'/eg/staff/cat/volcopy/t_attr_edit'"+'"></div>',
+        scope: { },
+        controller : ['$scope','itemSvc','egCore',
+            function ( $scope , itemSvc , egCore ) {
+
+                $scope.dirty = false;
+                $scope.template_controls = true;
+
+                $scope.fetchTemplates = function () {
+                    egCore.hatch.getItem('cat.copy.templates').then(function(t) {
+                        if (t) {
+                            $scope.templates = t;
+                            $scope.template_name_list = Object.keys(t);
+                        }
+                    });
+                }
+                $scope.fetchTemplates();
+            
+                $scope.applyTemplate = function (n) {
+                    angular.forEach($scope.templates[n], function (v,k) {
+                        $scope.working[k] = angular.copy(v);
+                    });
+                }
+            
+                $scope.deleteTemplate = function (n) {
+                    if (n) {
+                        delete $scope.templates[n]
+                        $scope.template_name_list = Object.keys($scope.templates);
+                        $scope.template_name = '';
+                        egCore.hatch.setItem('cat.copy.templates', $scope.templates);
+                        $scope.$parent.fetchTemplates();
+                    }
+                }
+
+                $scope.saveTemplate = function (n) {
+                    if (n) {
+                        var tmpl = {};
+            
+                        angular.forEach($scope.working, function (v,k) {
+                            if (angular.isObject(v)) { // we'll use the pkey
+                                if (v.id) v = v.id();
+                                else if (v.code) v = v.code();
+                            }
+            
+                            tmpl[k] = v;
+                        });
+            
+                        $scope.templates[n] = tmpl;
+                        $scope.template_name_list = Object.keys($scope.templates);
+            
+                        egCore.hatch.setItem('cat.copy.templates', $scope.templates);
+                        $scope.$parent.fetchTemplates();
+                    }
+                }
+            
+                $scope.templates = {};
+                $scope.template_name = '';
+                $scope.template_name_list = [];
+            
+                $scope.tracker = function (x,f) { if (x) return x[f]() };
+                $scope.idTracker = function (x) { if (x) return $scope.tracker(x,'id') };
+                $scope.cant_have_vols = function (id) { return !egCore.org.CanHaveVolumes(id); };
+            
+                $scope.orgById = function (id) { return egCore.org.get(id) }
+                $scope.statusById = function (id) {
+                    return $scope.status_list.filter( function (s) { return s.id() == id } )[0];
+                }
+                $scope.locationById = function (id) {
+                    return $scope.location_cache[''+id];
+                }
+            
+                createSimpleUpdateWatcher = function (field) {
+                    $scope.$watch('working.' + field, function () {
+                        var newval = $scope.working[field];
+            
+                        if (typeof newval != 'undefined') {
+                            $scope.dirty = true;
+                            if (angular.isObject(newval)) { // we'll use the pkey
+                                if (newval.id) $scope.working[field] = newval.id();
+                                else if (newval.code) $scope.working[field] = newval.code();
+                            }
+            
+                            if (newval == "") {
+                                $scope.working[field] = undefined;
+                            }
+            
+                        }
+                    });
+                }
+            
+                $scope.clearWorking = function () {
+                    angular.forEach($scope.working, function (v,k) {
+                        if (typeof v != 'undefined')
+                            $scope.working[k] = undefined;
+                    });
+                }
+            
+                $scope.working = {};
+                $scope.location_orgs = [];
+                $scope.location_cache = {};
+            
+                $scope.location_list = [];
+                itemSvc.get_locations(
+                    egCore.org.fullPath( egCore.auth.user().ws_ou(), true )
+                ).then(function(list){
+                    $scope.location_list = list;
+                });
+                createSimpleUpdateWatcher('location');
+            
+                $scope.status_list = [];
+                itemSvc.get_statuses().then(function(list){
+                    $scope.status_list = list;
+                });
+                createSimpleUpdateWatcher('status');
+            
+                $scope.circ_modifier_list = [];
+                itemSvc.get_circ_mods().then(function(list){
+                    $scope.circ_modifier_list = list;
+                });
+                createSimpleUpdateWatcher('circ_modifier');
+            
+                $scope.circ_type_list = [];
+                itemSvc.get_circ_types().then(function(list){
+                    $scope.circ_type_list = list;
+                });
+                createSimpleUpdateWatcher('circ_as_type');
+            
+                $scope.age_protect_list = [];
+                itemSvc.get_age_protects().then(function(list){
+                    $scope.age_protect_list = list;
+                });
+                createSimpleUpdateWatcher('age_protect');
+            
+                createSimpleUpdateWatcher('circ_lib');
+                createSimpleUpdateWatcher('circulate');
+                createSimpleUpdateWatcher('holdable');
+                createSimpleUpdateWatcher('fine_level');
+                createSimpleUpdateWatcher('loan_duration');
+                createSimpleUpdateWatcher('cost');
+                createSimpleUpdateWatcher('deposit');
+                createSimpleUpdateWatcher('deposit_amount');
+                createSimpleUpdateWatcher('mint_condition');
+                createSimpleUpdateWatcher('opac_visible');
+                createSimpleUpdateWatcher('ref');
+            }
+        ]
+    }
+})
+
 

commit 6059f36449888a5c5604e92cea4e54da64f1c118
Author: Mike Rylander <mrylander at gmail.com>
Date:   Thu Aug 20 22:17:20 2015 -0400

    webstaff: improvements to core UI widgets
    
    * Add enter-key-handler
    * improve basic combo box
    * teach org selector how to not have a default value
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/services/ui.js b/Open-ILS/web/js/ui/default/staff/services/ui.js
index 5291d9a..966b3e3 100644
--- a/Open-ILS/web/js/ui/default/staff/services/ui.js
+++ b/Open-ILS/web/js/ui/default/staff/services/ui.js
@@ -218,10 +218,10 @@ function($modal, $interpolate) {
         template:
             '<div class="input-group">'+
                 '<input type="text" class="form-control" ng-model="selected">'+
-                '<div class="input-group-btn">'+
-                    '<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown"><span class="caret"></span></button>'+
-                    '<ul class="dropdown-menu dropdown-menu-right" role="menu">'+
-                        '<li ng-repeat="item in list" class="input-lg"><a href="#" ng-click="changeValue(item)">{{item}}</a></li>'+
+                '<div class="input-group-btn" dropdown>'+
+                    '<button type="button" class="btn btn-default dropdown-toggle"><span class="caret"></span></button>'+
+                    '<ul class="dropdown-menu dropdown-menu-right">'+
+                        '<li ng-repeat="item in list"><a href ng-click="changeValue(item)">{{item}}</a></li>'+
                     '</ul>'+
                 '</div>'+
             '</div>',
@@ -246,8 +246,9 @@ function($modal, $interpolate) {
         transclude : true,
         replace : true, // makes styling easier
         scope : {
-            selected : '=', // defaults to workstation or root org
-            
+            selected : '=', // defaults to workstation or root org,
+                            // unless the nodefault attibute exists
+
             // Each org unit is passed into this function and, for
             // any org units where the response value is true, the
             // org unit will not be added to the selector.
@@ -286,6 +287,9 @@ function($modal, $interpolate) {
         controller : ['$scope','$timeout','egOrg','egAuth',
               function($scope , $timeout , egOrg , egAuth) {
 
+            $scope.egOrg = egOrg; // for use in the link function
+            $scope.egAuth = egAuth; // for use in the link function
+
             // avoid linking the full fleshed tree to the scope by 
             // tossing in a flattened list.
             $scope.orgList = egOrg.list().map(function(org) {
@@ -307,12 +311,41 @@ function($modal, $interpolate) {
                 if ($scope.onchange) $scope.onchange($scope.selected);
             }
 
-            if (!$scope.selected)
-                $scope.selected = egOrg.get(egAuth.user().ws_ou());
-        }]
+        }],
+        link : function(scope, element, attrs, egGridCtrl) {
+
+            // boolean fields are presented as value-less attributes
+            angular.forEach(
+                ['nodefault'],
+                function(field) {
+                    if (angular.isDefined(attrs[field]))
+                        scope[field] = true;
+                    else
+                        scope[field] = false;
+                }
+            );
+
+            if (!scope.selected && !scope.nodefault)
+                scope.selected = scope.egOrg.get(scope.egAuth.user().ws_ou());
+        }
+
     }
 })
 
+/* http://eric.sau.pe/angularjs-detect-enter-key-ngenter/ */
+.directive('egEnter', function () {
+    return function (scope, element, attrs) {
+        element.bind("keydown keypress", function (event) {
+            if(event.which === 13) {
+                scope.$apply(function (){
+                    scope.$eval(attrs.egEnter);
+                });
+ 
+                event.preventDefault();
+            }
+        });
+    };
+})
 
 /*
 http://stackoverflow.com/questions/18061757/angular-js-and-html5-date-input-value-how-to-get-firefox-to-show-a-readable-d

commit 52fc0b11335ec839f0a607524f6e1ecee26e4442
Author: Mike Rylander <mrylander at gmail.com>
Date:   Wed Aug 19 13:43:23 2015 -0400

    webstaff: Add a "Batch Apply" template row above call numbers
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2 b/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2
index b616e03..496d853 100644
--- a/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2
@@ -3,26 +3,57 @@
 <style> select { width: 80px } </style>
 
 <accordion close-others="false">
-    <accordion-group heading="Volume Editor" is-open="show_vols">
+    <accordion-group heading="Volume/Copy Details" is-open="show_vols">
 
         <div class="container-fluid">
+            <div class="row bg-info">
+                <div class="col-xs-2"><h4 class="center-block">[% l('Batch Apply') %]</h4></div>
+                <div class="col-xs-10">
+                    <div class="container-fluid">
+                        <div class="row">
+                            <div class="col-xs-1">
+                                <select ng-model="batch.classification" ng-options="cl.id() as cl.name() for cl in classification_list">
+                                    <option value="">[% l('(Unset)') %]</option>
+                                </select>
+                            </div>
+                            <div class="col-xs-1">
+                                <select ng-model="batch.prefix" ng-options="p.id() as p.label() for p in prefix_list">
+                                    <option value="">[% l('(Unset)') %]</option>
+                                </select>
+                            </div>
+                            <div class="col-xs-3">
+                                <input type="text" ng-model="batch.label"/>
+                            </div>
+                            <div class="col-xs-1">
+                                <select ng-model="batch.suffix" ng-options="s.id() as s.label() for s in suffix_list">
+                                    <option value="">[% l('(Unset)') %]</option>
+                                </select>
+                            </div>
+                            <div class="col-xs-1"></div>
+                            <div class="col-xs-5">
+                                <button class="btn btn-default center-block" ng-click="applyBatchCNValues()" type="button">Apply</button>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div> <!-- row -->
             <div class="row">
-                <div class="col-xs-1">[% l('Library') %]</div>
-                <div class="col-xs-1">[% l('Volumes') %]</div>
+                <div class="col-xs-1"><h5>[% l('Library') %]</h5></div>
+                <div class="col-xs-1"><h5>[% l('Volumes') %]</h5></div>
                 <div class="col-xs-10">
                     <div class="container-fluid">
                         <div class="row">
-                            <div class="col-xs-1">[% l('Classification') %]</div>
-                            <div class="col-xs-1">[% l('Prefix') %]</div>
-                            <div class="col-xs-3">[% l('Call Number') %]</div>
-                            <div class="col-xs-1">[% l('Suffix') %]</div>
-                            <div class="col-xs-1">[% l('Copies') %]</div>
+                            <div class="col-xs-1"><h5>[% l('Classification') %]</h5></div>
+                            <div class="col-xs-1"><h5>[% l('Prefix') %]</h5></div>
+                            <div class="col-xs-3"><h5>[% l('Call Number') %]</h5></div>
+                            <div class="col-xs-1"><h5>[% l('Suffix') %]</h5></div>
+                            <div class="col-xs-1"><h5>[% l('Copies') %]</h5></div>
                             <div class="col-xs-5">
                                 <div class="container-fluid">
                                     <div class="row">
-                                        <div class="col-xs-6">[% l('Barcode') %]</div>
-                                        <div class="col-xs-3">[% l('Copy #') %]</div>
-                                        <div class="col-xs-3">[% l('Part') %]</div>
+                                        <div class="col-xs-6"><h5>[% l('Barcode') %]</h5></div>
+                                        <div class="col-xs-3"><h5>[% l('Copy #') %]</h5></div>
+                                        <div class="col-xs-3"><h5>[% l('Part') %]</h5></div>
                                     </div>
                                 </div>
                             </div>
@@ -34,7 +65,7 @@
         </div> <!-- container -->
 
     </accordion-group>
-    <accordion-group heading="Copy Editor" is-open="show_copies">
+    <accordion-group heading="Copy Attributes" is-open="show_copies">
 
         <ul ng-model="copytab" class="nav nav-tabs">
           <li ng-class="{active : copytab == 'working'}">
@@ -82,7 +113,7 @@
 
                                 <div class="row">
                                     <div class="col-md-12">
-                                        <b>[% l('Circulate?') %]</b>
+                                        <h5>[% l('Circulate?') %]</h5>
                                     </div>
                                 </div>
                                 <div class="row">
@@ -108,7 +139,7 @@
 
                                 <div class="row">
                                     <div class="col-md-12">
-                                        <b>[% l('Circulation Library') %]</b>
+                                        <h5>[% l('Circulation Library') %]</h5>
                                     </div>
                                 </div>
                                 <div class="row">
@@ -119,7 +150,7 @@
 
                                 <div class="row">
                                     <div class="col-md-12">
-                                        <b>[% l('Shelving Location') %]</b>
+                                        <h5>[% l('Shelving Location') %]</h5>
                                     </div>
                                 </div>
                                 <div class="row">
@@ -133,7 +164,7 @@
 
                                 <div class="row">
                                     <div class="col-md-12">
-                                        <b>[% l('Circulation Modifier') %]</b>
+                                        <h5>[% l('Circulation Modifier') %]</h5>
                                     </div>
                                 </div>
                                 <div class="row">
@@ -147,7 +178,7 @@
 
                                 <div class="row">
                                     <div class="col-md-12">
-                                        <b>[% l('Circulate as Type') %]</b>
+                                        <h5>[% l('Circulate as Type') %]</h5>
                                     </div>
                                 </div>
                                 <div class="row">
@@ -161,7 +192,7 @@
 
                                 <div class="row">
                                     <div class="col-md-12">
-                                        <b>[% l('Holdable?') %]</b>
+                                        <h5>[% l('Holdable?') %]</h5>
                                     </div>
                                 </div>
                                 <div class="row">
@@ -187,7 +218,7 @@
 
                                 <div class="row">
                                     <div class="col-md-12">
-                                        <b>[% l('Loan Duration') %]</b>
+                                        <h5>[% l('Loan Duration') %]</h5>
                                     </div>
                                 </div>
                                 <div class="row">
@@ -202,7 +233,7 @@
 
                                 <div class="row">
                                     <div class="col-md-12">
-                                        <b>[% l('Fine Level') %]</b>
+                                        <h5>[% l('Fine Level') %]</h5>
                                     </div>
                                 </div>
                                 <div class="row">
@@ -217,7 +248,7 @@
 
                                 <div class="row">
                                     <div class="col-md-12">
-                                        <b>[% l('Age-based Hold Protection') %]</b>
+                                        <h5>[% l('Age-based Hold Protection') %]</h5>
                                     </div>
                                 </div>
                                 <div class="row">
@@ -237,7 +268,7 @@
 
                                 <div class="row">
                                     <div class="col-md-12">
-                                        <b>[% l('Status') %]</b>
+                                        <h5>[% l('Status') %]</h5>
                                     </div>
                                 </div>
                                 <div class="row">
@@ -251,7 +282,7 @@
 
                                 <div class="row">
                                     <div class="col-md-12">
-                                        <b>[% l('Reference?') %]</b>
+                                        <h5>[% l('Reference?') %]</h5>
                                     </div>
                                 </div>
                                 <div class="row">
@@ -277,7 +308,7 @@
 
                                 <div class="row">
                                     <div class="col-md-12">
-                                        <b>[% l('OPAC Visible?') %]</b>
+                                        <h5>[% l('OPAC Visible?') %]</h5>
                                     </div>
                                 </div>
                                 <div class="row">
@@ -303,7 +334,7 @@
 
                                 <div class="row">
                                     <div class="col-md-12">
-                                        <b>[% l('Price') %]</b>
+                                        <h5>[% l('Price') %]</h5>
                                     </div>
                                 </div>
                                 <div class="row">
@@ -314,7 +345,7 @@
 
                                 <div class="row">
                                     <div class="col-md-12">
-                                        <b>[% l('Cost') %]</b>
+                                        <h5>[% l('Cost') %]</h5>
                                     </div>
                                 </div>
                                 <div class="row">
@@ -325,7 +356,7 @@
 
                                 <div class="row">
                                     <div class="col-md-12">
-                                        <b>[% l('Deposit?') %]</b>
+                                        <h5>[% l('Deposit?') %]</h5>
                                     </div>
                                 </div>
                                 <div class="row">
@@ -351,7 +382,7 @@
 
                                 <div class="row">
                                     <div class="col-md-12">
-                                        <b>[% l('Deposit Amount') %]</b>
+                                        <h5>[% l('Deposit Amount') %]</h5>
                                     </div>
                                 </div>
                                 <div class="row">
@@ -362,7 +393,7 @@
 
                                 <div class="row">
                                     <div class="col-md-12">
-                                        <b>[% l('Quality') %]</b>
+                                        <h5>[% l('Quality') %]</h5>
                                     </div>
                                 </div>
                                 <div class="row">
@@ -393,7 +424,7 @@
                             <div class="container-fluid"> <!-- third column -->
                                 <div class="row">
                                     <div class="col-md-12">
-                                        <b>[% l('Statistical Catagories') %]</b>
+                                        <h5>[% l('Statistical Catagories') %]</h5>
                                     </div>
                                 </div>
                             </div>
diff --git a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
index 33c1928..cbad7dc 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
@@ -49,16 +49,10 @@ function(egCore , $q) {
     };
 
     service.get_prefixes = function(org) {
-        if (egCore.env.acnp)
-            return $q.when(egCore.env.acnp.list);
-
         return egCore.pcrud.search('acnp',
             {owning_lib : egCore.org.fullPath(org, true)},
             null, {atomic : true}
-        ).then(function(list) {
-            egCore.env.absorbList(list, 'acnp');
-            return list;
-        });
+        );
 
     };
 
@@ -70,16 +64,10 @@ function(egCore , $q) {
     };
 
     service.get_suffixes = function(org) {
-        if (egCore.env.acns)
-            return $q.when(egCore.env.acns.list);
-
         return egCore.pcrud.search('acns',
             {owning_lib : egCore.org.fullPath(org, true)},
             null, {atomic : true}
-        ).then(function(list) {
-            egCore.env.absorbList(list, 'acns');
-            return list;
-        });
+        );
 
     };
 
@@ -204,7 +192,7 @@ function(egCore , $q) {
                 $scope.updateBarcode = function () { $scope.copy.barcode($scope.barcode); $scope.copy.ischanged(1); };
                 $scope.updateCopyNo = function () { $scope.copy.copy_number($scope.copy_number); $scope.copy.ischanged(1); };
                 $scope.updatePart = function () {
-                    var p = angular.filter($scope.part_list, function (x) {
+                    var p = $scope.part_list.filter(function (x) {
                         return x.label() == $scope.part
                     });
                     if (p.length > 0) { // preexisting part
@@ -308,11 +296,6 @@ function(egCore , $q) {
                     });
                 }
 
-                $scope.classification = $scope.callNumber.label_class();
-                $scope.prefix = $scope.callNumber.prefix();
-                $scope.suffix = $scope.callNumber.suffix();
-
-                $scope.label = $scope.callNumber.label();
                 $scope.updateLabel = function () {
                     angular.forEach($scope.copies, function(cp) {
                         cp.call_number().label($scope.label);
@@ -320,6 +303,42 @@ function(egCore , $q) {
                     });
                 }
 
+                $scope.$watch('callNumber.prefix()', function (v) {
+                    if (typeof v != 'object') {
+                        $scope.prefix = $scope.prefix_list.filter(function (p) {
+                            return p.id() == v;
+                        })[0];
+                        $scope.callNumber.prefix($scope.prefix);
+                    }
+                });
+
+                $scope.$watch('callNumber.suffix()', function (v) {
+                    if (typeof v != 'object') {
+                        $scope.suffix = $scope.suffix_list.filter( function (s) {
+                            return s.id() == v;
+                        })[0];
+                        $scope.callNumber.suffix($scope.suffix);
+                    }
+                });
+
+                $scope.$watch('callNumber.label_class()', function (v) {
+                    if (typeof v != 'object') {
+                        $scope.classification = $scope.classification_list.filter(function (c) {
+                            return c.id() == v;
+                        })[0];
+                        $scope.callNumber.label_class($scope.classification);
+                    }
+                });
+
+                $scope.$watch('callNumber.label()', function (v) {
+                    $scope.label = v;
+                });
+
+                $scope.prefix = $scope.callNumber.prefix();
+                $scope.suffix = $scope.callNumber.suffix();
+                $scope.classification = $scope.callNumber.label_class();
+                $scope.label = $scope.callNumber.label();
+
                 $scope.copy_count = $scope.copies.length;
                 $scope.orig_copy_count = $scope.copy_count;
 
@@ -503,6 +522,26 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
         $scope.completed_copies = [];
         $scope.location_orgs = [];
         $scope.location_cache = {};
+        $scope.batch = {};
+
+        $scope.applyBatchCNValues = function () {
+            if ($scope.data.tree) {
+                angular.forEach($scope.data.tree, function(cn_hash) {
+                    angular.forEach(cn_hash, function(copies) {
+                        angular.forEach(copies, function(cp) {
+                            if (typeof $scope.batch.classification != 'undefined' && $scope.batch.classification != '')
+                                cp.call_number().label_class($scope.batch.classification);
+                            if (typeof $scope.batch.prefix != 'undefined' && $scope.batch.prefix != '')
+                                cp.call_number().prefix($scope.batch.prefix);
+                            if (typeof $scope.batch.label != 'undefined' && $scope.batch.label != '')
+                                cp.call_number().label($scope.batch.label);
+                            if (typeof $scope.batch.suffix != 'undefined' && $scope.batch.suffix != '')
+                                cp.call_number().suffix($scope.batch.suffix);
+                        });
+                    });
+                });
+            }
+        }
 
         $scope.completedGridDataProvider = egGridDataProvider.instance({
             get : function(offset, count) {
@@ -592,6 +631,21 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
             $scope.workingGridDataProvider.refresh();
         });
 
+        $scope.suffix_list = [];
+        itemSvc.get_suffixes(egCore.auth.user().ws_ou()).then(function(list){
+            $scope.suffix_list = list;
+        });
+
+        $scope.prefix_list = [];
+        itemSvc.get_prefixes(egCore.auth.user().ws_ou()).then(function(list){
+            $scope.prefix_list = list;
+        });
+
+        $scope.classification_list = [];
+        itemSvc.get_classifications().then(function(list){
+            $scope.classification_list = list;
+        });
+
         $scope.$watch('completed_copies.length', function () {
             $scope.completedGridDataProvider.refresh();
         });

commit 695c0935b328f96ac992bab3d75786dece7e9380
Author: Mike Rylander <mrylander at gmail.com>
Date:   Tue Aug 18 17:53:48 2015 -0400

    webstaff: Update all instances of the call number, mainly for copy display
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
index 5127c4f..33c1928 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
@@ -279,26 +279,46 @@ function(egCore , $q) {
                 itemSvc.get_suffixes($scope.callNumber.owning_lib()).then(function(list){
                     $scope.suffix_list = list;
                 });
-                $scope.updateSuffix = function () { $scope.callNumber.suffix($scope.suffix); $scope.callNumber.ischanged(1); };
+                $scope.updateSuffix = function () {
+                    angular.forEach($scope.copies, function(cp) {
+                        cp.call_number().suffix($scope.suffix);
+                        cp.call_number().ischanged(1);
+                    });
+                }
 
                 $scope.prefix_list = [];
                 itemSvc.get_prefixes($scope.callNumber.owning_lib()).then(function(list){
                     $scope.prefix_list = list;
                 });
-                $scope.updatePrefix = function () { $scope.callNumber.prefix($scope.prefix); $scope.callNumber.ischanged(1); };
+                $scope.updatePrefix = function () {
+                    angular.forEach($scope.copies, function(cp) {
+                        cp.call_number().prefix($scope.prefix);
+                        cp.call_number().ischanged(1);
+                    });
+                }
 
                 $scope.classification_list = [];
                 itemSvc.get_classifications().then(function(list){
                     $scope.classification_list = list;
                 });
-                $scope.updateClassification = function () { $scope.callNumber.label_class($scope.classification); $scope.callNumber.ischanged(1); };
+                $scope.updateClassification = function () {
+                    angular.forEach($scope.copies, function(cp) {
+                        cp.call_number().label_class($scope.classification);
+                        cp.call_number().ischanged(1);
+                    });
+                }
 
                 $scope.classification = $scope.callNumber.label_class();
                 $scope.prefix = $scope.callNumber.prefix();
                 $scope.suffix = $scope.callNumber.suffix();
 
                 $scope.label = $scope.callNumber.label();
-                $scope.updateLabel = function () { $scope.callNumber.label($scope.label); $scope.callNumber.ischanged(1); };
+                $scope.updateLabel = function () {
+                    angular.forEach($scope.copies, function(cp) {
+                        cp.call_number().label($scope.label);
+                        cp.call_number().ischanged(1);
+                    });
+                }
 
                 $scope.copy_count = $scope.copies.length;
                 $scope.orig_copy_count = $scope.copy_count;

commit 24380867ab3cca203ab3768822f4d3393ec76dc6
Author: Mike Rylander <mrylander at gmail.com>
Date:   Tue Aug 18 17:27:47 2015 -0400

    webstaff: Copy editing!
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2 b/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2
index 7ccd43f..b616e03 100644
--- a/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2
+++ b/Open-ILS/src/templates/staff/cat/volcopy/t_edit.tt2
@@ -5,7 +5,7 @@
 <accordion close-others="false">
     <accordion-group heading="Volume Editor" is-open="show_vols">
 
-        <div collapse="hide_vols" class="container-fluid">
+        <div class="container-fluid">
             <div class="row">
                 <div class="col-xs-1">[% l('Library') %]</div>
                 <div class="col-xs-1">[% l('Volumes') %]</div>
@@ -36,18 +36,34 @@
     </accordion-group>
     <accordion-group heading="Copy Editor" is-open="show_copies">
 
-    <div collapse="hide_vols" class="container-fluid">
+        <ul ng-model="copytab" class="nav nav-tabs">
+          <li ng-class="{active : copytab == 'working'}">
+            <a ng-click="copytab='working'" >[% l('Working Copies') %]</a>
+          </li>
+          <li ng-class="{active : copytab == 'complete'}">
+            <a ng-click="copytab='complete'" >[% l('Completed Copies') %]</a>
+          </li>
+        </ul>
+
+<div class="tab-content">
+  <div class="tab-pane active">
+    <div ng-show="copytab == 'working'">
+
+    <div class="container-fluid"> <!-- working copy editor -->
         <div class="row">
             <div class="col-lg-4">
 
                 <eg-grid
                   id-field="id"
                   idl-class="acp"
-                  features="startSelected,-pagination,-actions,-menu,-picker,-index"
-                  main-label="[% l('Working Copies') %]"
+                  features="startSelected,-pagination,-actions,-picker,-index"
                   items-provider="workingGridDataProvider"
                   grid-controls="workingGridControls"
                   persist-key="cat.volcopy.copies">
+
+                  <eg-grid-menu-item handler="workingToComplete"
+                   label="[% l('Store Selected') %]"></eg-grid-menu-item>
+
                 
                   <eg-grid-field label="[% l('Barcode') %]"     path='barcode' visible></eg-grid-field>
                   <eg-grid-field label="[% l('Created') %]"     path="create_date" visible></eg-grid-field>
@@ -63,6 +79,7 @@
 
                         <div class="col-md-4">
                             <div class="container-fluid"> <!-- first column -->
+
                                 <div class="row">
                                     <div class="col-md-12">
                                         <b>[% l('Circulate?') %]</b>
@@ -88,11 +105,136 @@
                                         </div>
                                     </div>
                                 </div>
-                            </div>
+
+                                <div class="row">
+                                    <div class="col-md-12">
+                                        <b>[% l('Circulation Library') %]</b>
+                                    </div>
+                                </div>
+                                <div class="row">
+                                    <div class="col-md-12">
+                                        <eg-org-selector selected="working.circ_lib" disableTest="cant_have_vols"></eg-org-selector>
+                                    </div>
+                                </div>
+
+                                <div class="row">
+                                    <div class="col-md-12">
+                                        <b>[% l('Shelving Location') %]</b>
+                                    </div>
+                                </div>
+                                <div class="row">
+                                    <div class="col-md-12">
+                                        <select
+                                            ng-model="working.location"
+                                            ng-options="l.name() for l in location_list track by idTracker(l)"
+                                        ></select>
+                                    </div>
+                                </div>
+
+                                <div class="row">
+                                    <div class="col-md-12">
+                                        <b>[% l('Circulation Modifier') %]</b>
+                                    </div>
+                                </div>
+                                <div class="row">
+                                    <div class="col-md-12">
+                                        <select
+                                            ng-model="working.circ_modifier"
+                                            ng-options="m.name() for m in circ_mod_list track by tracker(m,'code')"
+                                        ></select>
+                                    </div>
+                                </div>
+
+                                <div class="row">
+                                    <div class="col-md-12">
+                                        <b>[% l('Circulate as Type') %]</b>
+                                    </div>
+                                </div>
+                                <div class="row">
+                                    <div class="col-md-12">
+                                        <select
+                                            ng-model="working.circ_as_type"
+                                            ng-options="t.value() for t in circ_type_list track by tracker(t,'code')"
+                                        ></select>
+                                    </div>
+                                </div>
+
+                                <div class="row">
+                                    <div class="col-md-12">
+                                        <b>[% l('Holdable?') %]</b>
+                                    </div>
+                                </div>
+                                <div class="row">
+                                    <div class="col-md-12">
+                                        <div class="container-fluid">
+                                            <div class="row">
+                                                <div class="col-xs-6">
+                                                    <label>
+                                                        <input type="radio" ng-model="working.holdable" value="t"/>
+                                                        [% l('Yes') %]
+                                                    </label>
+                                                </div>
+                                                <div class="col-xs-6">
+                                                    <label>
+                                                        <input type="radio" ng-model="working.holdable" value="f"/>
+                                                        [% l('No') %]
+                                                    </label>
+                                                </div>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+
+                                <div class="row">
+                                    <div class="col-md-12">
+                                        <b>[% l('Loan Duration') %]</b>
+                                    </div>
+                                </div>
+                                <div class="row">
+                                    <div class="col-md-12">
+                                        <select ng-model="working.loan_duration">
+                                            <option value="1">[% l('Short') %]</option>
+                                            <option value="2" selected>[% l('Normal') %]</option>
+                                            <option value="3">[% l('Extended') %]</option>
+                                        </select>
+                                    </div>
+                                </div>
+
+                                <div class="row">
+                                    <div class="col-md-12">
+                                        <b>[% l('Fine Level') %]</b>
+                                    </div>
+                                </div>
+                                <div class="row">
+                                    <div class="col-md-12">
+                                        <select ng-model="working.fine_level">
+                                            <option value="1">[% l('Low') %]</option>
+                                            <option value="2" selected>[% l('Normal') %]</option>
+                                            <option value="3">[% l('High') %]</option>
+                                        </select>
+                                    </div>
+                                </div>
+
+                                <div class="row">
+                                    <div class="col-md-12">
+                                        <b>[% l('Age-based Hold Protection') %]</b>
+                                    </div>
+                                </div>
+                                <div class="row">
+                                    <div class="col-md-12">
+                                        <select
+                                            ng-model="working.age_protect"
+                                            ng-options="a.name() for a in age_protect_list track by idTracker(a)"
+                                        ></select>
+                                    </div>
+                                </div>
+
+                            </div> <!-- first column -->
                         </div>
 
                         <div class="col-md-4">
                             <div class="container-fluid"> <!-- second column -->
+
                                 <div class="row">
                                     <div class="col-md-12">
                                         <b>[% l('Status') %]</b>
@@ -102,11 +244,148 @@
                                     <div class="col-md-12">
                                         <select
                                             ng-model="working.status"
-                                            ng-change="updateWorkingStatus()"
                                             ng-options="s.name() for s in status_list track by idTracker(s)"
                                         ></select>
                                     </div>
                                 </div>
+
+                                <div class="row">
+                                    <div class="col-md-12">
+                                        <b>[% l('Reference?') %]</b>
+                                    </div>
+                                </div>
+                                <div class="row">
+                                    <div class="col-md-12">
+                                        <div class="container-fluid">
+                                            <div class="row">
+                                                <div class="col-xs-6">
+                                                    <label>
+                                                        <input type="radio" ng-model="working.ref" value="t"/>
+                                                        [% l('Yes') %]
+                                                    </label>
+                                                </div>
+                                                <div class="col-xs-6">
+                                                    <label>
+                                                        <input type="radio" ng-model="working.ref" value="f"/>
+                                                        [% l('No') %]
+                                                    </label>
+                                                </div>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+
+                                <div class="row">
+                                    <div class="col-md-12">
+                                        <b>[% l('OPAC Visible?') %]</b>
+                                    </div>
+                                </div>
+                                <div class="row">
+                                    <div class="col-md-12">
+                                        <div class="container-fluid">
+                                            <div class="row">
+                                                <div class="col-xs-6">
+                                                    <label>
+                                                        <input type="radio" ng-model="working.opac_visible" value="t"/>
+                                                        [% l('Yes') %]
+                                                    </label>
+                                                </div>
+                                                <div class="col-xs-6">
+                                                    <label>
+                                                        <input type="radio" ng-model="working.opac_visible" value="f"/>
+                                                        [% l('No') %]
+                                                    </label>
+                                                </div>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+
+                                <div class="row">
+                                    <div class="col-md-12">
+                                        <b>[% l('Price') %]</b>
+                                    </div>
+                                </div>
+                                <div class="row">
+                                    <div class="col-md-12">
+                                        <input ng-model="working.price" type="text"/>
+                                    </div>
+                                </div>
+
+                                <div class="row">
+                                    <div class="col-md-12">
+                                        <b>[% l('Cost') %]</b>
+                                    </div>
+                                </div>
+                                <div class="row">
+                                    <div class="col-md-12">
+                                        <input ng-model="working.cost" type="text"/>
+                                    </div>
+                                </div>
+
+                                <div class="row">
+                                    <div class="col-md-12">
+                                        <b>[% l('Deposit?') %]</b>
+                                    </div>
+                                </div>
+                                <div class="row">
+                                    <div class="col-md-12">
+                                        <div class="container-fluid">
+                                            <div class="row">
+                                                <div class="col-xs-6">
+                                                    <label>
+                                                        <input type="radio" ng-model="working.deposit" value="t"/>
+                                                        [% l('Yes') %]
+                                                    </label>
+                                                </div>
+                                                <div class="col-xs-6">
+                                                    <label>
+                                                        <input type="radio" ng-model="working.deposit" value="f"/>
+                                                        [% l('No') %]
+                                                    </label>
+                                                </div>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+
+                                <div class="row">
+                                    <div class="col-md-12">
+                                        <b>[% l('Deposit Amount') %]</b>
+                                    </div>
+                                </div>
+                                <div class="row">
+                                    <div class="col-md-12">
+                                        <input ng-model="working.deposit_amount" type="text"/>
+                                    </div>
+                                </div>
+
+                                <div class="row">
+                                    <div class="col-md-12">
+                                        <b>[% l('Quality') %]</b>
+                                    </div>
+                                </div>
+                                <div class="row">
+                                    <div class="col-md-12">
+                                        <div class="container-fluid">
+                                            <div class="row">
+                                                <div class="col-xs-6">
+                                                    <label>
+                                                        <input type="radio" ng-model="working.mint_condition" value="t"/>
+                                                        [% l('Good') %]
+                                                    </label>
+                                                </div>
+                                                <div class="col-xs-6">
+                                                    <label>
+                                                        <input type="radio" ng-model="working.mint_condition" value="f"/>
+                                                        [% l('Damaged') %]
+                                                    </label>
+                                                </div>
+                                            </div>
+                                        </div>
+                                    </div>
+                                </div>
+
                             </div>
                         </div>
 
@@ -126,5 +405,39 @@
         </div>
     </div>
 
+    </div>
+    <div ng-show="copytab == 'complete'">
+       <eg-grid
+         id-field="id"
+         idl-class="acp"
+         features="-pagination,-actions,-picker,-index"
+         items-provider="completedGridDataProvider"
+         grid-controls="completedGridControls"
+         persist-key="cat.volcopy.copies.complete">
+       
+         <eg-grid-menu-item handler="completeToWorking"
+          label="[% l('Edit Selected') %]"></eg-grid-menu-item>
+
+         <eg-grid-menu-item handler="saveAndExit"
+          label="[% l('Save & Exit') %]"></eg-grid-menu-item>
+
+         <eg-grid-field label="[% l('Barcode') %]"     path='barcode' visible></eg-grid-field>
+         <eg-grid-field label="[% l('Created') %]"     path="create_date" visible></eg-grid-field>
+         <eg-grid-field label="[% l('Activated') %]"   path="active_date" visible></eg-grid-field>
+         <eg-grid-field label="[% l('Call Number') %]" path="call_number.label" visible></eg-grid-field>
+         <eg-grid-field label="[% l('Circ Library') %]" flesher="orgById" path="circ_lib.name" visible></eg-grid-field>
+         <eg-grid-field label="[% l('Shelving Location') %]" flesher="locationById" path="location.name" visible></eg-grid-field>
+         <eg-grid-field label="[% l('Circ Modifier') %]" path="circ_modifier" visible></eg-grid-field>
+         <eg-grid-field label="[% l('Circulate?') %]"  path="circulate" visible></eg-grid-field>
+         <eg-grid-field label="[% l('Holdable?') %]"   path="circulate" visible></eg-grid-field>
+         <eg-grid-field label="[% l('Reference?') %]"  path="ref" visible></eg-grid-field>
+         <eg-grid-field label="[% l('Status') %]"      flesher="statusById" path="status.name" visible></eg-grid-field>
+         <eg-grid-field label="[% l('OPAC Visible') %]" path="opac_visible" visible></eg-grid-field>
+       
+       </eg-grid>
+    </div>
+  </div>
+</div>
+
     </accordion-group>
 </accordion>
diff --git a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
index 59a736e..5127c4f 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
@@ -62,6 +62,13 @@ function(egCore , $q) {
 
     };
 
+    service.get_locations = function(orgs) {
+        return egCore.pcrud.search('acpl',
+            {owning_lib : orgs},
+            null, {atomic : true}
+        );
+    };
+
     service.get_suffixes = function(org) {
         if (egCore.env.acns)
             return $q.when(egCore.env.acns.list);
@@ -89,6 +96,45 @@ function(egCore , $q) {
 
     };
 
+    service.get_circ_mods = function() {
+        if (egCore.env.ccm)
+            return $q.when(egCore.env.ccm.list);
+
+        return egCore.pcrud.retrieveAll('ccm', {}, {atomic : true}).then(
+            function(list) {
+                egCore.env.absorbList(list, 'ccm');
+                return list;
+            }
+        );
+
+    };
+
+    service.get_circ_types = function() {
+        if (egCore.env.citm)
+            return $q.when(egCore.env.citm.list);
+
+        return egCore.pcrud.retrieveAll('citm', {}, {atomic : true}).then(
+            function(list) {
+                egCore.env.absorbList(list, 'citm');
+                return list;
+            }
+        );
+
+    };
+
+    service.get_age_protects = function() {
+        if (egCore.env.crahp)
+            return $q.when(egCore.env.crahp.list);
+
+        return egCore.pcrud.retrieveAll('crahp', {}, {atomic : true}).then(
+            function(list) {
+                egCore.env.absorbList(list, 'crahp');
+                return list;
+            }
+        );
+
+    };
+
     service.bmp_parts = {};
     service.get_parts = function(rec) {
         if (service.bmp_parts[rec])
@@ -107,7 +153,7 @@ function(egCore , $q) {
     service.flesh = {   
         flesh : 3, 
         flesh_fields : {
-            acp : ['call_number','parts','location'],
+            acp : ['call_number','parts'],
             acn : ['label_class','prefix','suffix']
         }
     }
@@ -314,6 +360,11 @@ function(egCore , $q) {
                 $scope.orig_cn_count = $scope.cn_count;
 
                 $scope.owning_lib = egCore.org.get($scope.lib);
+                $scope.$watch('owning_lib', function (l) {
+                    angular.forEach( $scope.struct[$scope.first_cn], function (cp) {
+                        cp.call_number().owning_lib( $scope.owning_lib.id() );
+                    });
+                });
 
                 $scope.cant_have_vols = function (id) { return !egCore.org.CanHaveVolumes(id); };
 
@@ -324,13 +375,13 @@ function(egCore , $q) {
                             var cn = new egCore.idl.acn();
                             cn.id( --$scope.new_cn_id );
                             cn.isnew( true );
-                            cn.owning_lib( $scope.owning_lib );
+                            cn.owning_lib( $scope.owning_lib.id() );
                             cn.record( $scope.full_cn.record() );
 
                             var cp = new egCore.idl.acp();
                             cp.id( --$scope.new_cp_id );
                             cp.isnew( true );
-                            cp.circ_lib( $scope.owning_lib );
+                            cp.circ_lib( $scope.owning_lib.id() );
                             cp.call_number( cn );
 
                             $scope.struct[cn.id()] = [cp];
@@ -369,7 +420,52 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
     $scope.show_vols = true;
     $scope.show_copies = true;
 
-    $scope.idTracker = function (x) { if (x) return x.id() };
+    $scope.tracker = function (x,f) { if (x) return x[f]() };
+    $scope.idTracker = function (x) { if (x) return $scope.tracker(x,'id') };
+    $scope.cant_have_vols = function (id) { return !egCore.org.CanHaveVolumes(id); };
+
+    $scope.orgById = function (id) { return egCore.org.get(id) }
+    $scope.statusById = function (id) {
+        return $scope.status_list.filter( function (s) { return s.id() == id } )[0];
+    }
+    $scope.locationById = function (id) {
+        return $scope.location_cache[''+id];
+    }
+
+    $scope.workingToComplete = function () {
+        angular.forEach( $scope.workingGridControls.selectedItems(), function (c) {
+            angular.forEach( itemSvc.copies, function (w, i) {
+                if (c === w)
+                    $scope.completed_copies = $scope.completed_copies.concat(itemSvc.copies.splice(i,1));
+            });
+        });
+    }
+
+    $scope.completeToWorking = function () {
+        angular.forEach( $scope.completedGridControls.selectedItems(), function (c) {
+            angular.forEach( $scope.completed_copies, function (w, i) {
+                if (c === w)
+                    itemSvc.copies = itemSvc.copies.concat($scope.completed_copies.splice(i,1));
+            });
+        });
+    }
+
+    createSimpleUpdateWatcher = function (field) {
+        $scope.$watch('working.' + field, function () {
+            var newval = $scope.working[field];
+            if (angular.isObject(newval)) { // we'll use the pkey
+                if (newval.id) newval = newval.id();
+                else if (newval.code) newval = newval.code();
+            }
+
+            if ($scope.workingGridControls && $scope.workingGridControls.selectedItems) {
+                angular.forEach(
+                    $scope.workingGridControls.selectedItems(),
+                    function (cp) { cp[field](newval); cp.ischanged(1); }
+                );
+            }
+        });
+    }
 
     $timeout(function(){
 
@@ -379,10 +475,23 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
     if (dataKey && dataKey.length > 0) {
         $scope.working = {};
 
+        $scope.copytab = 'working';
         $scope.tab = 'edit';
         $scope.summaryRecord = null;
         $scope.record_id = null;
         $scope.data = {};
+        $scope.completed_copies = [];
+        $scope.location_orgs = [];
+        $scope.location_cache = {};
+
+        $scope.completedGridDataProvider = egGridDataProvider.instance({
+            get : function(offset, count) {
+                //return provider.arrayNotifier(itemSvc.copies, offset, count);
+                return this.arrayNotifier($scope.completed_copies, offset, count);
+            }
+        });
+
+        $scope.completedGridControls = {};
 
         $scope.workingGridDataProvider = egGridDataProvider.instance({
             get : function(offset, count) {
@@ -431,20 +540,83 @@ function($scope , $q , $routeParams , $location , $timeout , egCore , egNet , eg
         });
 
         $scope.$watch('data.copies.length', function () {
-            console.log('data.copies.length changed');
+            if ($scope.data.copies) {
+                var base_orgs = $scope.data.copies.map(function(cp){
+                    return cp.circ_lib()
+                }).filter(function(e,i,a){
+                    return a.lastIndexOf(e) === i;
+                });
+
+                var all_orgs = [];
+                angular.forEach(base_orgs, function(o) {
+                    all_orgs = all_orgs.concat( egCore.org.fullPath(o, true) );
+                });
+
+                var final_orgs = all_orgs.filter(function(e,i,a){
+                    return a.lastIndexOf(e) === i;
+                }).sort(function(a,b){return b-a});
+
+                if ($scope.location_orgs.toString() != final_orgs.toString()) {
+                    $scope.location_orgs = final_orgs;
+                    if ($scope.location_orgs.length) {
+                        itemSvc.get_locations($scope.location_orgs).then(function(list){
+                            angular.forEach(list, function(l) {
+                                $scope.location_cache[ ''+l.id() ] = l;
+                            });
+                            $scope.location_list = list;
+                        });
+                    }
+                }
+            }
+
             $scope.workingGridDataProvider.refresh();
         });
 
+        $scope.$watch('completed_copies.length', function () {
+            $scope.completedGridDataProvider.refresh();
+        });
+
+        $scope.location_list = [];
+        itemSvc.get_locations().then(function(list){
+            $scope.location_list = list;
+        });
+        createSimpleUpdateWatcher('location');
+
         $scope.status_list = [];
         itemSvc.get_statuses().then(function(list){
             $scope.status_list = list;
         });
-        $scope.updateWorkingStatus = function () {
-            angular.forEach(
-                $scope.workingGridControls.selectedItems(),
-                function (cp) { cp.status($scope.working.status.id()); cp.ischanged(1); }
-            );
-        };
+        createSimpleUpdateWatcher('status');
+
+        $scope.circ_mod_list = [];
+        itemSvc.get_circ_mods().then(function(list){
+            $scope.circ_mod_list = list;
+        });
+        createSimpleUpdateWatcher('circ_modifier');
+
+        $scope.circ_type_list = [];
+        itemSvc.get_circ_types().then(function(list){
+            $scope.circ_type_list = list;
+        });
+        createSimpleUpdateWatcher('circ_as_type');
+
+        $scope.age_protect_list = [];
+        itemSvc.get_age_protects().then(function(list){
+            $scope.age_protect_list = list;
+        });
+        createSimpleUpdateWatcher('age_protect');
+
+        createSimpleUpdateWatcher('circulate');
+        createSimpleUpdateWatcher('holdable');
+        createSimpleUpdateWatcher('fine_level');
+        createSimpleUpdateWatcher('loan_duration');
+        createSimpleUpdateWatcher('cost');
+        createSimpleUpdateWatcher('deposit');
+        createSimpleUpdateWatcher('deposit_amount');
+        createSimpleUpdateWatcher('mint_condition');
+        createSimpleUpdateWatcher('opac_visible');
+        createSimpleUpdateWatcher('ref');
+
     }
 
     });

commit 73c91cca7b0b55cb30a6b29a51e7afdaa0135b1a
Author: Mike Rylander <mrylander at gmail.com>
Date:   Tue Aug 18 17:26:58 2015 -0400

    webstaff: Teach the grid how to flesh the last step in a path if it is unfleshed
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Jason Stephenson <jstephenson at mvlc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/services/grid.js b/Open-ILS/web/js/ui/default/staff/services/grid.js
index c93bca2..b7c1ce3 100644
--- a/Open-ILS/web/js/ui/default/staff/services/grid.js
+++ b/Open-ILS/web/js/ui/default/staff/services/grid.js
@@ -979,6 +979,7 @@ angular.module('egGridMod',
         require : '^egGrid',
         restrict : 'AE',
         scope : {
+            flesher: '=', // optional; function that can flesh a linked field, given the value
             name  : '@', // required; unique name
             path  : '@', // optional; flesh path
             ignore: '@', // optional; fields to ignore when path is a wildcard
@@ -1241,6 +1242,7 @@ angular.module('egGridMod',
         // the fields over that we need (so the scope object can go away).
         cols.cloneFromScope = function(colSpec) {
             return {
+                flesher  : colSpec.flesher,
                 name  : colSpec.name,
                 label : colSpec.label,
                 path  : colSpec.path,
@@ -1421,15 +1423,18 @@ angular.module('egGridMod',
             // TODO: consider a caching layer to speed up template 
             // rendering, particularly for nested objects?
             gridData.itemFieldValue = function(item, column) {
+                var val;
                 if (column.name in item) {
                     if (typeof item[column.name] == 'function') {
-                        return item[column.name]();
+                        val = item[column.name]();
                     } else {
-                        return item[column.name];
+                        val = item[column.name];
                     }
                 } else {
-                    return gridData.nestedItemFieldValue(item, column);
+                    val = gridData.nestedItemFieldValue(item, column);
                 }
+
+                return val;
             }
 
             // TODO: deprecate me
@@ -1445,6 +1450,8 @@ angular.module('egGridMod',
             // value is an IDL field, run the value through its
             // corresponding output filter.
             gridData.nestedItemFieldValue = function(obj, column) {
+                item = obj; // keep a copy around
+
                 if (obj === null || obj === undefined || obj === '') return '';
                 if (!column.path) return obj;
 
@@ -1453,9 +1460,13 @@ angular.module('egGridMod',
 
                 angular.forEach(parts, function(step, idx) {
                     // object is not fleshed to the expected extent
-                    if (!obj || typeof obj != 'object') {
-                        obj = '';
-                        return;
+                    if (typeof obj != 'object') {
+                        if (typeof obj != 'undefined' && column.flesher) {
+                            obj = column.flesher(obj, column, item);
+                        } else {
+                            obj = '';
+                            return;
+                        }
                     }
 
                     var cls = obj.classname;

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

Summary of changes:
 Open-ILS/examples/fm_IDL.xml                       |   14 +-
 .../src/perlmods/lib/OpenILS/Application/Cat.pm    |   77 +-
 .../lib/OpenILS/Application/Cat/AssetCommon.pm     |   68 +-
 .../lib/OpenILS/Application/Search/Authority.pm    |   11 +-
 .../Application/Storage/Publisher/authority.pm     |    6 +-
 .../perlmods/lib/OpenILS/Application/SuperCat.pm   |    3 +-
 Open-ILS/src/perlmods/lib/OpenILS/WWW/SuperCat.pm  |   30 +-
 Open-ILS/src/sql/Pg/002.schema.config.sql          |    2 +-
 Open-ILS/src/sql/Pg/011.schema.authority.sql       |  169 ++-
 Open-ILS/src/sql/Pg/950.data.seed-values.sql       |   20 +
 Open-ILS/src/sql/Pg/999.functions.global.sql       |    4 +-
 ...filter_authority_browse_search_by_thesaurus.sql |  603 +++++++++
 ...0943.data.additional_authority_fixed_fields.sql |   26 +
 Open-ILS/src/templates/staff/base_js.tt2           |    1 +
 .../src/templates/staff/cat/bucket/copy/index.tt2  |    6 +
 .../src/templates/staff/cat/bucket/copy/t_view.tt2 |    6 +
 .../templates/staff/cat/bucket/record/index.tt2    |    6 +
 .../staff/cat/bucket/record/t_edit_lead_record.tt2 |   16 +
 .../staff/cat/bucket/record/t_merge_records.tt2    |   40 +
 .../templates/staff/cat/bucket/record/t_view.tt2   |    6 +
 Open-ILS/src/templates/staff/cat/catalog/index.tt2 |   19 +
 .../staff/cat/catalog/t_add_to_bucket.tt2          |   39 +
 .../templates/staff/cat/catalog/t_authority.tt2    |    3 +
 .../src/templates/staff/cat/catalog/t_catalog.tt2  |   13 +
 .../staff/cat/catalog/t_conjoined_items.tt2        |   35 +
 .../staff/cat/catalog/t_conjoined_selector.tt2     |   26 +
 .../src/templates/staff/cat/catalog/t_holdings.tt2 |   38 +-
 .../src/templates/staff/cat/catalog/t_new_bib.tt2  |   13 +
 .../staff/cat/catalog/t_request_items.tt2          |   48 +
 .../staff/cat/item/replace_barcode/index.tt2       |   36 +-
 .../staff/cat/share/t_authority_browser.tt2        |   63 +
 .../staff/cat/share/t_authority_link_dialog.tt2    |   15 +
 .../staff/cat/share/t_authority_linker.tt2         |   22 +
 .../staff/cat/share/t_edit_new_authority.tt2       |   15 +
 .../src/templates/staff/cat/share/t_marcedit.tt2   |   68 +-
 .../staff/cat/share/t_physchar_dialog.tt2          |   15 +
 .../staff/cat/share/t_physchar_wizard.tt2          |   55 +
 .../staff/cat/share/t_replace_barcode.tt2          |   45 +
 Open-ILS/src/templates/staff/cat/volcopy/index.tt2 |    1 -
 .../templates/staff/cat/volcopy/t_attr_edit.tt2    |  411 ++++++
 .../templates/staff/cat/volcopy/t_copy_notes.tt2   |   91 ++
 .../src/templates/staff/cat/volcopy/t_defaults.tt2 |  318 +++++
 .../src/templates/staff/cat/volcopy/t_edit.tt2     |  261 ++--
 .../src/templates/staff/cat/volcopy/t_view.tt2     |   15 +-
 .../staff/cat/z3950/t_edit_overlay_record.tt2      |    2 +-
 Open-ILS/src/templates/staff/cat/z3950/t_list.tt2  |    7 +-
 .../src/templates/staff/cat/z3950/t_marc_edit.tt2  |    4 +-
 .../src/templates/staff/cat/z3950/t_target.tt2     |    2 +-
 .../templates/staff/circ/share/hold_strings.tt2    |    4 +-
 Open-ILS/src/templates/staff/css/cat.css.tt2       |   13 +
 Open-ILS/src/templates/staff/navbar.tt2            |    6 +
 Open-ILS/src/templates/staff/share/t_autogrid.tt2  |    1 +
 Open-ILS/web/js/dojo/openils/Util.js               |   14 +
 Open-ILS/web/js/ui/default/cat/authority/list.js   |    8 +
 .../web/js/ui/default/staff/cat/bucket/copy/app.js |  156 +++-
 .../js/ui/default/staff/cat/bucket/record/app.js   |   83 ++-
 .../web/js/ui/default/staff/cat/catalog/app.js     |  870 +++++++++++-
 .../js/ui/default/staff/cat/services/marcedit.js   |  692 ++++++++++-
 .../js/ui/default/staff/cat/services/tagtable.js   |  373 +++++-
 .../web/js/ui/default/staff/cat/services/z3950.js  |   29 +-
 .../web/js/ui/default/staff/cat/volcopy/app.js     | 1429 ++++++++++++++++++--
 Open-ILS/web/js/ui/default/staff/cat/z3950/app.js  |    7 +-
 .../web/js/ui/default/staff/circ/patron/app.js     |   10 +-
 .../web/js/ui/default/staff/circ/services/holds.js |   25 +
 Open-ILS/web/js/ui/default/staff/marcrecord.js     |   47 +-
 .../web/js/ui/default/staff/services/coresvc.js    |    2 +-
 Open-ILS/web/js/ui/default/staff/services/grid.js  |  101 ++-
 Open-ILS/web/js/ui/default/staff/services/idl.js   |   63 +
 Open-ILS/web/js/ui/default/staff/services/ui.js    |   79 +-
 Open-ILS/web/js/ui/default/staff/services/user.js  |   24 +
 .../Cataloging/web_staff_client_sprint2.txt        |   10 +-
 71 files changed, 6332 insertions(+), 508 deletions(-)
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/0942.schema.filter_authority_browse_search_by_thesaurus.sql
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/0943.data.additional_authority_fixed_fields.sql
 create mode 100644 Open-ILS/src/templates/staff/cat/bucket/record/t_edit_lead_record.tt2
 create mode 100644 Open-ILS/src/templates/staff/cat/bucket/record/t_merge_records.tt2
 create mode 100644 Open-ILS/src/templates/staff/cat/catalog/t_add_to_bucket.tt2
 create mode 100644 Open-ILS/src/templates/staff/cat/catalog/t_authority.tt2
 create mode 100644 Open-ILS/src/templates/staff/cat/catalog/t_conjoined_items.tt2
 create mode 100644 Open-ILS/src/templates/staff/cat/catalog/t_conjoined_selector.tt2
 create mode 100644 Open-ILS/src/templates/staff/cat/catalog/t_new_bib.tt2
 create mode 100644 Open-ILS/src/templates/staff/cat/catalog/t_request_items.tt2
 create mode 100644 Open-ILS/src/templates/staff/cat/share/t_authority_browser.tt2
 create mode 100644 Open-ILS/src/templates/staff/cat/share/t_authority_link_dialog.tt2
 create mode 100644 Open-ILS/src/templates/staff/cat/share/t_authority_linker.tt2
 create mode 100644 Open-ILS/src/templates/staff/cat/share/t_edit_new_authority.tt2
 create mode 100644 Open-ILS/src/templates/staff/cat/share/t_physchar_dialog.tt2
 create mode 100644 Open-ILS/src/templates/staff/cat/share/t_physchar_wizard.tt2
 create mode 100644 Open-ILS/src/templates/staff/cat/share/t_replace_barcode.tt2
 create mode 100644 Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2
 create mode 100644 Open-ILS/src/templates/staff/cat/volcopy/t_copy_notes.tt2
 create mode 100644 Open-ILS/src/templates/staff/cat/volcopy/t_defaults.tt2


hooks/post-receive
-- 
Evergreen ILS


More information about the open-ils-commits mailing list