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

Evergreen Git git at git.evergreen-ils.org
Sat Mar 10 19:58:58 EST 2012


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  e6eb4059934177a0929737d774233454a4c52b8b (commit)
       via  9ff41d494e268f981db189e1cf01d1df04b37cae (commit)
       via  82868632f2478d1d554fb0433418e9227df3a52c (commit)
       via  01e0058a678bc8642d12ded314b95dc66d85a3a8 (commit)
       via  08b79d69c9df220cdc0b3920880d40e3b1051071 (commit)
       via  ddc8c42d6a98013ddfb81840d566e06ef0551fd6 (commit)
       via  32458ad85b11e517eca78ebb3aead3300e31b4b2 (commit)
       via  fb1a10e370bf708b1f4fb1c59dc029ed75a85d94 (commit)
       via  c9dae1827b4e6d01fa78b10343a9530aeca3f824 (commit)
      from  4a34d78093052fde921d5c73ba96673515441b8b (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 e6eb4059934177a0929737d774233454a4c52b8b
Author: Dan Scott <dan at coffeecode.net>
Date:   Sat Mar 10 19:53:47 2012 -0500

    Add styles to org selector for loc groups
    
    Looks like user agents don't let us do a lot with styling OPTION
    elements (text colour, background colour) but this gives sites that want
    to further distinguish copy groups from libraries in the org selector
    something to work with.
    
    Signed-off-by: Dan Scott <dan at coffeecode.net>

diff --git a/Open-ILS/src/templates/opac/parts/org_selector.tt2 b/Open-ILS/src/templates/opac/parts/org_selector.tt2
index 5b55e90..02c2448 100644
--- a/Open-ILS/src/templates/opac/parts/org_selector.tt2
+++ b/Open-ILS/src/templates/opac/parts/org_selector.tt2
@@ -24,6 +24,7 @@ BLOCK build_org_selector;
             org_unit = node.org;
             loc_grp = node.loc_grp;
             ou_id = org_unit.id;
+            css_class = '';
             disabled = '';
             selected = '';
 
@@ -57,6 +58,11 @@ BLOCK build_org_selector;
 
             node_value = ou_id;
             IF loc_grp; node_value = node_value _ ':' _ loc_grp.id; END;
+            IF loc_grp;
+                css_class = 'class="loc_grp"';
+            ELSE;
+                css_class = 'class="org_unit"';
+            END;
 
             IF can_have_vols_only AND org_unit.ou_type.can_have_vols != 't';
                 disabled = 'disabled="disabled"';
@@ -64,7 +70,7 @@ BLOCK build_org_selector;
                 selected = 'selected="selected"';
             END %] 
 
-            <option value='[% node_value %]' [% selected %] [% disabled %]> 
+            <option value='[% node_value %]' [% selected %] [% disabled %] [% css_class %]> 
             [%
                 # loc_grp's are displayed as children of the current org
                 depth = org_unit.ou_type.depth;

commit 9ff41d494e268f981db189e1cf01d1df04b37cae
Author: Bill Erickson <berick at esilibrary.com>
Date:   Thu Mar 1 13:51:53 2012 -0500

    Copy Location Groups : sort to top option
    
    Adds a 'top' flag to copy_location_groups which, when enabled, will
    cause the location group to sort above the child org units in the org
    unit selector in the tpac.
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Dan Scott <dan at coffeecode.net>

diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml
index c7bb14d..94e1aa2 100644
--- a/Open-ILS/examples/fm_IDL.xml
+++ b/Open-ILS/examples/fm_IDL.xml
@@ -3935,6 +3935,7 @@ SELECT  usr,
 			<field reporter:label="Is OPAC Visible?" name="opac_visible" reporter:datatype="bool"/>
 			<field reporter:label="Owning Org Unit" name="owner"  reporter:datatype="org_unit"/>
             <field reporter:label="Position" name="pos" reporter:datatype="int"/>
+            <field reporter:label="Display Above Orgs" name="top" reporter:datatype="bool"/>
             <field reporter:label="Copy Location Mappings" name="location_maps" oils_persist:virtual="true" reporter:datatype="link"/>
 		</fields>
 		<links>
diff --git a/Open-ILS/src/sql/Pg/040.schema.asset.sql b/Open-ILS/src/sql/Pg/040.schema.asset.sql
index 85d58f6..d98fc75 100644
--- a/Open-ILS/src/sql/Pg/040.schema.asset.sql
+++ b/Open-ILS/src/sql/Pg/040.schema.asset.sql
@@ -55,6 +55,7 @@ CREATE TABLE asset.copy_location_group (
     name            TEXT    NOT NULL, -- i18n
     owner           INT     NOT NULL REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
     pos             INT     NOT NULL DEFAULT 0,
+    top             BOOL    NOT NULL DEFAULT FALSE,
     opac_visible    BOOL    NOT NULL DEFAULT TRUE,
     CONSTRAINT lgroup_once_per_owner UNIQUE (owner,name)
 );
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_loc_search_groups.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_loc_search_groups.sql
index a6dd74a..9e393d9 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_loc_search_groups.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_loc_search_groups.sql
@@ -7,6 +7,7 @@ CREATE TABLE asset.copy_location_group (
     name            TEXT    NOT NULL, -- i18n
     owner           INT     NOT NULL REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
     pos             INT     NOT NULL DEFAULT 0,
+    top             BOOL    NOT NULL DEFAULT FALSE,
     opac_visible    BOOL    NOT NULL DEFAULT TRUE,
     CONSTRAINT lgroup_once_per_owner UNIQUE (owner,name)
 );
diff --git a/Open-ILS/src/templates/opac/parts/org_selector.tt2 b/Open-ILS/src/templates/opac/parts/org_selector.tt2
index a2c222e..5b55e90 100644
--- a/Open-ILS/src/templates/opac/parts/org_selector.tt2
+++ b/Open-ILS/src/templates/opac/parts/org_selector.tt2
@@ -29,15 +29,30 @@ BLOCK build_org_selector;
 
             NEXT UNLESS ctx.is_staff || org_unit.opac_visible == 't';
 
-            IF !loc_grp;
+            IF !loc_grp; # processing an org unit
+
+                top_loc_groups = [];
                 IF show_loc_groups;
+                    # add the location groups that sort below the child org units
                     FOR grp IN ctx.copy_location_groups.$ou_id.reverse;
-                        node_stack.push({org => org_unit, loc_grp => grp});
+                        IF grp.top == 't';
+                            top_loc_groups.push(grp);
+                        ELSE;
+                            node_stack.push({org => org_unit, loc_grp => grp});
+                        END;
                     END;
                 END;
+
+                # add the child org units
                 FOR child IN org_unit.children.reverse;
                     node_stack.push({org => child});
                 END;
+
+                # add the location groups that sort to the top
+                # above the child org units
+                FOR grp IN top_loc_groups;
+                    node_stack.push({org => org_unit, loc_grp => grp});
+                END;
             END;
 
             node_value = ou_id;

commit 82868632f2478d1d554fb0433418e9227df3a52c
Author: Bill Erickson <berick at esilibrary.com>
Date:   Thu Mar 1 10:18:49 2012 -0500

    TPac: detail page distinct copy/holdings display depth
    
    This adds a new CGI param "copy_depth" which the record detail page uses
    to determine which copies to display in the copy grid and what serials
    holdings data to show.
    
    The goal is to separate search depth from copy display depth for 2
    main reasons:
    
    1. When present, the search ou is set by the "locg" parameter (org +
    copy location group).  The "Show" links in the record detail page use
    "loc" to determine the copy location org (and subsequently the depth),
    which is ignored in the presence of "locg".  In other words, we need
    a different way to communicate which range of copies to display.
    
    2. Separating copy depth and search depth allows us to display
    search location-related summary information while at the same time
    displaying a broader set of copy information.  For example, searching
    BR1, we can see copy summary info for BR1, SYS1, and CONS even when
    explicitly viewing copy information for CONS.  In other words, viewing a
    broader set of copies for a record does not change the search/context
    org unit, it only extends the set of copies to display.
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Dan Scott <dan at coffeecode.net>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Record.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Record.pm
index 3d1d6c8..2daaf2f 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Record.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Record.pm
@@ -15,7 +15,14 @@ sub load_record {
     $ctx->{page} = 'record';  
 
     my $org = $self->_get_search_lib();
-    my $depth = $self->cgi->param('depth') || $ctx->{get_aou}->($org)->ou_type->depth;
+    my $depth = $self->cgi->param('depth');
+    $depth = $ctx->{get_aou}->($org)->ou_type->depth 
+        unless defined $depth; # can be 0
+
+    my $copy_depth = $self->cgi->param('copy_depth');
+    $copy_depth = $depth unless defined $copy_depth; # can be 0
+    $self->ctx->{copy_depth} = $copy_depth;
+
     my $copy_limit = int($self->cgi->param('copy_limit') || 10);
     my $copy_offset = int($self->cgi->param('copy_offset') || 0);
 
@@ -34,7 +41,7 @@ sub load_record {
     my $cstore = OpenSRF::AppSession->create('open-ils.cstore');
     my $copy_rec = $cstore->request(
         'open-ils.cstore.json_query.atomic', 
-        $self->mk_copy_query($rec_id, $org, $depth, $copy_limit, $copy_offset)
+        $self->mk_copy_query($rec_id, $org, $copy_depth, $copy_limit, $copy_offset)
     );
 
     my (undef, @rec_data) = $self->get_records_and_facets([$rec_id], undef, {flesh => '{holdings_xml,bmp,mra,acp,acnp,acns}'});
@@ -47,6 +54,7 @@ sub load_record {
 
     $ctx->{have_holdings_to_show} = 0;
     $ctx->{have_mfhd_to_show} = 0;
+
     $self->get_hold_copy_summary($rec_id, $org);
 
     $cstore->kill_me;
@@ -56,7 +64,7 @@ sub load_record {
             ($org, "opac.fully_compressed_serial_holdings")
     ) {
         $ctx->{holding_summaries} =
-            $self->get_holding_summaries($rec_id, $org, $depth);
+            $self->get_holding_summaries($rec_id, $org, $copy_depth);
 
         $ctx->{have_holdings_to_show} =
             scalar(@{$ctx->{holding_summaries}->{basic}}) ||
@@ -64,7 +72,7 @@ sub load_record {
             scalar(@{$ctx->{holding_summaries}->{supplement}});
     } else {
         $ctx->{mfhd_summaries} =
-            $self->get_mfhd_summaries($rec_id, $org, $depth);
+            $self->get_mfhd_summaries($rec_id, $org, $copy_depth);
 
         if ($ctx->{mfhd_summaries} && scalar(@{$ctx->{mfhd_summaries}})
         ) {
@@ -78,7 +86,7 @@ sub load_record {
         },
         issues => sub {
             $ctx->{expanded_holdings} =
-                $self->get_expanded_holdings($rec_id, $org, $depth)
+                $self->get_expanded_holdings($rec_id, $org, $copy_depth)
                 if $ctx->{have_holdings_to_show};
         },
         cnbrowse => sub {
diff --git a/Open-ILS/src/templates/opac/parts/record/copy_counts.tt2 b/Open-ILS/src/templates/opac/parts/record/copy_counts.tt2
index defa0ae..f9f5594 100644
--- a/Open-ILS/src/templates/opac/parts/record/copy_counts.tt2
+++ b/Open-ILS/src/templates/opac/parts/record/copy_counts.tt2
@@ -8,10 +8,10 @@
             ou_id = ctx.copy_summary.$depth.org_unit;
     %]
     <li>
-    [% l('[quant,_1,copy,copies] at [_2].', ou_avail, ctx.get_aou(ou_id).name)
-        | html %]
-    [%- IF ou_avail > 0 && ou_id != ctx.search_ou; %]
-    <a href="[% mkurl('', {loc => ou_id}); %]"
+    [% l('[quant,_1,copy,copies] at [_2].', ou_avail, ctx.get_aou(ou_id).name) | html %]
+    [%- this_depth = ctx.get_aou(ou_id).ou_type.depth;
+        IF ou_avail > 0 && this_depth != ctx.copy_depth %]
+    <a href="[% mkurl('', {copy_depth => this_depth}, ['copy_offset']); %]"
        title="[% l('Show copies at [_1]', ctx.get_aou(ou_id).name); %]">
        [%- l('(Show)'); %]</a>
     [%- END; %]

commit 01e0058a678bc8642d12ded314b95dc66d85a3a8
Author: Bill Erickson <berick at esilibrary.com>
Date:   Fri Feb 24 15:43:47 2012 -0500

    Copy Location Search Groups : admin UI improvements
    
    Two improvements to the location group list.
    
    1. Replace x / checkmark glyphs with clearer Visible / Not Visible
    labels.  Thanks for the suggestions, Lebbeous.
    
    2. Vertically align the edit actions block and the drag grips so the
    actions are always in the same place.
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Dan Scott <dan at coffeecode.net>

diff --git a/Open-ILS/src/templates/conify/global/asset/copy_location_group.tt2 b/Open-ILS/src/templates/conify/global/asset/copy_location_group.tt2
index c62ec4a..5466e11 100644
--- a/Open-ILS/src/templates/conify/global/asset/copy_location_group.tt2
+++ b/Open-ILS/src/templates/conify/global/asset/copy_location_group.tt2
@@ -112,22 +112,26 @@
 <div class='hidden'>
     <div dojoType='openils.widget.ProgressDialog' jsId='progressDialog'></div>
     <div id='dnd-drag-actions'>
-        <span>
-            <span style='padding:5px;' group="GRPID">
-                <span><a href="javascript:;" onclick="drawGroupEntries(GRPID)">GRPNAME</a></span>
-                [
-                <span>[% l('Visible') %]
-                    <span style='color:green' name='visible'>&#x2713;</span>
-                    <span style='color:red' name='invisible'>&#x2717;</span>
+        <table width='100%'><tr>
+            <td nowrap='nowrap' align='left'>
+                <span style='padding:5px;' group="GRPID">
+                    <span><a href="javascript:;" onclick="drawGroupEntries(GRPID)">GRPNAME</a></span>
                 </span>
+            </td>
+            <td align='right'>
+                [
+                <span style='color:green' name='visible'>[% l('Visible') %]</span>
+                <span style='color:red' name='invisible'>[% l('Not Visible') %]</span>
                 <span><a href="javascript:;" onclick="editGroup(GRPID)">[% l('Edit') %]</a></span>
                 <span><a href="javascript:;" onclick="deleteGroup(GRPID)">[% l('Delete') %]</a></span>
                 ]
-            </span>
-            <span></span>
-            <span class='acplg-drag-handle'></span>
-            <span class='acplg-drag-handle'></span>
-            <span class='acplg-drag-handle'></span>
+            </td>
+            <td align='right' width='32px'>
+                <span class='acplg-drag-handle'></span>
+                <span class='acplg-drag-handle'></span>
+                <span class='acplg-drag-handle'></span>
+            </td>
+        </tr></table>
         </span>
     </div>
     <div id='acplg-edit-dialog'></div>

commit 08b79d69c9df220cdc0b3920880d40e3b1051071
Author: Bill Erickson <berick at esilibrary.com>
Date:   Wed Feb 22 10:31:37 2012 -0500

    Copy Location Search Groups : Release Notes
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Lebbeous Fogle-Weekley <lebbeous at esilibrary.com>
    Signed-off-by: Dan Scott <dan at coffeecode.net>

diff --git a/docs/RELEASE_NOTES_2_2.txt b/docs/RELEASE_NOTES_2_2.txt
index ecd0808..fc38aa5 100644
--- a/docs/RELEASE_NOTES_2_2.txt
+++ b/docs/RELEASE_NOTES_2_2.txt
@@ -73,6 +73,22 @@ following steps:
 New features
 ------------
 
+OPAC
+~~~~
+
+Copy Location Groups
+^^^^^^^^^^^^^^^^^^^^
+This feature allows staff to create and name sets of copy locations to use as
+a search filter in the catalog.  OPAC-visible groups will display within the
+library selector in the template toolkit OPAC.  When a user selects a group
+and performs a search, the set of results will be limited to records that have
+copies in one of the copy locations within the group.  Groups can live at any
+level of the library hierarchy and may include copy locations from any parent 
+org unit or child org unit.
+
+For advanced users, this change includes a new Query Parser filter called
+location_groups().
+
 Cataloging
 ~~~~~~~~~~
 

commit ddc8c42d6a98013ddfb81840d566e06ef0551fd6
Author: Bill Erickson <berick at esilibrary.com>
Date:   Fri Feb 17 16:14:16 2012 -0500

    Copy Location Search Groups : Admin UI
    
    Added admin UI for managing copy location groups.  It allows the user to
    create/edit/delete groups and add copy locations to the groups.
    
    The UI can be found under Admin -> Local System Administration -> Copy
    Location Groups.
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Dan Scott <dan at coffeecode.net>

diff --git a/Open-ILS/src/templates/conify/global/asset/copy_location_group.tt2 b/Open-ILS/src/templates/conify/global/asset/copy_location_group.tt2
new file mode 100644
index 0000000..c62ec4a
--- /dev/null
+++ b/Open-ILS/src/templates/conify/global/asset/copy_location_group.tt2
@@ -0,0 +1,137 @@
+[% WRAPPER base.tt2 %]
+[% ctx.page_title = l('Copy Location Group') %]
+<script type="text/javascript" src='[% ctx.media_prefix %]/js/ui/default/conify/global/asset/copy_location_group.js'> </script>
+<link rel='stylesheet' type='text/css' href='[% ctx.media_prefix %]/js/dojo/dojo/resources/dnd.css'/>
+<style>
+    .acpl-content {
+        padding: 10px; 
+        padding-top: 20px;
+        min-width: 20%;
+        height: 100%;
+    }
+    .acpl-content div:first-child {
+        font-weight: bold;
+        font-size: 110%;
+        border-bottom: 1px solid #888;
+        background-color:#E7A555;
+    }
+    .acpl-content-2 {
+        border-left: 2px solid #888;
+        float:left;
+    }
+    .acplg-drag-handle { 
+        background-image: url([% ctx.media_prefix %]/images/dimple.png);
+        background-repeat: no-repeat;
+        background-position: center;
+        padding: 5px;
+        margin-left:5px;
+    }
+    .acplg-list-tbody td {
+        padding: 2px;
+    }
+    .acplg-list-tbody tr:nth-child(even) {
+        background: #EEE
+    }
+    #acplg-list li {
+        padding: 5px;
+    }
+    #acplg-header {
+        margin-top: 20px;
+        border-bottom: 2px solid #888;
+        padding-bottom: 10px;
+    }
+    #acplg-header span:first-child {
+        font-weight: bold;
+        font-size: 130%;
+    }
+    .acplg-group-selected {
+        background-color:#E7A555;
+        border: 1px solid #4A4747;
+    }
+</style>
+
+<div id='acplg-header'>
+    <span>[% l('Copy Location Groups') %]</span>
+    <select dojoType="openils.widget.OrgUnitFilteringSelect"
+            jsId='contextOrgSelector'
+            searchAttr='shortname'
+            labelAttr='shortname'>
+    </select>
+</div>
+
+<div dojoType="dijit.layout.ContentPane" layoutAlign="client" style="width:100%">
+    <div class='acpl-content' style="float: left">
+        <div>
+            <table width='100%'><tr>
+                <td align='left' style='padding-left:5px;'>[% l('Location Groups') %]</td>
+                <td align='right'><button onclick='newGroup()'>[% l('New') %]</button></td>
+            </tr></table>
+        </div>
+        <ol id='acplg-list'></ol>
+    </div>
+    <div class='acpl-content acpl-content-2'>
+        <div>
+            <table width='100%'><tr>
+                <td align='left' style='padding-left:5px;'>[% l('Group Entries') %]</td>
+                <td align='right'><button onclick='editLocations("eliminate")'>[% l('Remove &rarr;') %]</button></td>
+            </tr></table>
+        </div>
+        <div style='height:400px; overflow-y:scroll'>
+            <table>
+                <tbody id='acplg-loc-map-tbody' class='acplg-list-tbody'>
+                    <tr id='acplg-loc-map-row'>
+                        <td><input type='checkbox' name='selector'/></td>
+                        <td><span name='owning_lib'></span></td>
+                        <td><span name='name'></span></td>
+                    </td>
+                </tbody>
+            </table>
+        </div>
+    </div>
+    <div class='acpl-content acpl-content-2'>
+        <div>
+            <table width='100%'><tr>
+                <td align='left'><button onclick='editLocations("create")'>[% l('&larr; Add') %]</button></td>
+                <td align='right' style='padding-right:5px;'>[% l('Copy Locations') %]</td>
+            </tr></table>
+        </div>
+        <div style='height:400px; overflow-y:scroll'>
+            <table>
+                <tbody id='acplg-loc-tbody' class='acplg-list-tbody'>
+                    <tr id='acplg-loc-row'>
+                        <td><input type='checkbox' name='selector'/></td>
+                        <td><span name='owning_lib'></span></td>
+                        <td><span name='name'></span></td>
+                    </td>
+                </tbody>
+            </table>
+        </div>
+    </div>
+</div>
+
+<div class='hidden'>
+    <div dojoType='openils.widget.ProgressDialog' jsId='progressDialog'></div>
+    <div id='dnd-drag-actions'>
+        <span>
+            <span style='padding:5px;' group="GRPID">
+                <span><a href="javascript:;" onclick="drawGroupEntries(GRPID)">GRPNAME</a></span>
+                [
+                <span>[% l('Visible') %]
+                    <span style='color:green' name='visible'>&#x2713;</span>
+                    <span style='color:red' name='invisible'>&#x2717;</span>
+                </span>
+                <span><a href="javascript:;" onclick="editGroup(GRPID)">[% l('Edit') %]</a></span>
+                <span><a href="javascript:;" onclick="deleteGroup(GRPID)">[% l('Delete') %]</a></span>
+                ]
+            </span>
+            <span></span>
+            <span class='acplg-drag-handle'></span>
+            <span class='acplg-drag-handle'></span>
+            <span class='acplg-drag-handle'></span>
+        </span>
+    </div>
+    <div id='acplg-edit-dialog'></div>
+</div>
+
+
+[% END %]
diff --git a/Open-ILS/web/js/ui/default/conify/global/asset/copy_location_group.js b/Open-ILS/web/js/ui/default/conify/global/asset/copy_location_group.js
new file mode 100644
index 0000000..9a9770b
--- /dev/null
+++ b/Open-ILS/web/js/ui/default/conify/global/asset/copy_location_group.js
@@ -0,0 +1,360 @@
+dojo.require('dijit.layout.ContentPane');
+dojo.require('dijit.layout.BorderContainer');
+dojo.require("dojo.dnd.Container");
+dojo.require("dojo.dnd.Source");
+dojo.require('fieldmapper.OrgUtils');
+dojo.require('openils.User');
+dojo.require('openils.Util');
+dojo.require('openils.Event');
+dojo.require('openils.PermaCrud');
+dojo.require('openils.widget.AutoGrid');
+dojo.require('openils.widget.ProgressDialog');
+dojo.require('openils.widget.OrgUnitFilteringSelect');
+dojo.require('openils.widget.EditDialog');
+
+var user;
+var groups;
+var locations;
+var source;
+var locTbody;
+var locRowTemplate;
+var locMapTbody;
+var locMapRowTemplate;
+var currentGroupId;
+var currentGroupMaps;
+var currentOrg;
+
+function init() {
+
+    user = new openils.User();
+   
+    // init the DnD environment
+    source = new dojo.dnd.Source('acplg-list');
+    dojo.connect(source, 'onDndDrop', updateGroupOrder);
+
+    // context org selector
+    user.buildPermOrgSelector(
+        'ADMIN_COPY_LOCATION_GROUP', 
+        contextOrgSelector, 
+        null, 
+        function() {
+            dojo.connect(contextOrgSelector, 'onChange', drawPage);
+        }
+    );
+
+    fetchCopyLocations();
+}
+
+function fetchCopyLocations() {
+    // the full set of copy locations can be very large.  
+    // Only retrieve the set of locations owned by orgs this user 
+    // can use for building location groups.
+    user.getPermOrgList(
+        ['ADMIN_COPY_LOCATION_GROUP'], 
+        function(list) {
+
+            var ownerOrgList = [];
+            dojo.forEach(list,
+                function(org) {
+                    // include parent orgs
+                    ownerOrgList = ownerOrgList.concat(org).concat(
+                        fieldmapper.aou.orgNodeTrail(fieldmapper.aou.findOrgUnit(org), true));
+                }
+            );
+
+            var pcrud = new openils.PermaCrud({authtoken : user.authtoken});
+            pcrud.search('acpl', // this can take some time...
+                {owning_lib : ownerOrgList},
+                {   
+                    async : true,
+                    join : 'aou',
+                    oncomplete : function(r) {
+                        locations = openils.Util.readResponse(r);
+                        sortCopyLocations();
+                        drawPage(user.user.ws_ou());
+                    }
+                }
+            );
+        },
+        true,
+        true
+    );
+}
+
+// sort the list of copy locations according the shape of 
+// the org unit tree.  apply a secondary sort on name.
+function sortCopyLocations() {
+    var newlist = [];
+
+    function addNode(node) {
+        // find locs for this org
+        var locs = locations.filter(function(loc) { return loc.owning_lib() == node.id() });
+        // sort on name and append to the new list
+        newlist = newlist.concat(locs.sort(function(a, b) { return a.name() < b.name() ? -1 : 1 }));
+        // repeat for org child nodes
+        dojo.forEach(node.children(), addNode);
+    }
+
+    addNode(fieldmapper.aou.globalOrgTree);
+    locations = newlist;
+}
+
+
+function drawPage(org) {
+    currentOrg = org;
+    currentGroupId = null;
+    currentGroupMaps = [];
+    //drawLocations();
+    drawGroupList();
+}
+
+function drawGroupList(selectedGrp) {
+    var pcrud = new openils.PermaCrud({authtoken : user.authtoken});
+    groups = pcrud.search('acplg', {owner : currentOrg}, {order_by : {acplg : 'pos'}});
+
+
+    source.selectAll();
+    source.deleteSelectedNodes();
+    source.clearItems();
+
+    dojo.forEach(groups,
+        function(group) {
+            if(!group) return;
+
+            var drag = dojo.byId('dnd-drag-actions').cloneNode(true);
+            drag.id = '';
+            var vis = openils.Util.isTrue(group.opac_visible());
+            openils.Util.hide(dojo.query('[name=' + (vis ? 'invisible' : 'visible') + ']', drag)[0]);
+
+
+            var node = source.insertNodes(false, [{ 
+                data : drag.innerHTML.replace(/GRPID/g, group.id()).replace(/GRPNAME/g, group.name()),
+                type : [group.id()+''] // use the type field to store the ID
+            }]);
+        }
+    );
+
+    if (groups.length == 0) {
+        selectedGrp = null
+    } else if (selectedGrp == null) {
+        selectedGrp = groups[0].id();
+    }
+
+    drawGroupEntries(selectedGrp);
+}
+
+function drawLocations() {
+
+    if (!locTbody) {
+        locTbody = dojo.byId('acplg-loc-tbody');
+        locRowTemplate = locTbody.removeChild(dojo.byId('acplg-loc-row'));
+    } else {
+        // clear out the previous table
+        while (node = locTbody.childNodes[0])
+            locTbody.removeChild(node);
+    }
+
+    var allMyOrgs = fieldmapper.aou.fullPath(currentOrg, true);
+
+    dojo.forEach(locations,
+        function(loc) {
+            if (allMyOrgs.indexOf(loc.owning_lib()) == -1) return;
+
+            // don't show locations contained in the current group
+            if (currentGroupMaps.length) {
+                var existing = currentGroupMaps.filter(
+                    function(map) { return (map.location() == loc.id()) });
+                if (existing.length > 0) return;
+            }
+
+            var row = locRowTemplate.cloneNode(true);
+            row.setAttribute('location', loc.id());
+            dojo.query('[name=name]', row)[0].innerHTML = loc.name();
+            dojo.query('[name=owning_lib]', row)[0].innerHTML = fieldmapper.aou.findOrgUnit(loc.owning_lib()).shortname();
+            locTbody.appendChild(row);
+        }
+    );
+}
+
+function updateGroupOrder() {
+    var pos = 0;
+    var toUpdate = [];
+
+    // find any groups that have changed position and send them off for update
+    dojo.forEach(
+        source.getAllNodes(),
+        function(node) {
+            var item = source.getItem(node.id);
+            var grpId = item.type[0];
+            var grp = groups.filter(function(g) { return g.id() == grpId })[0];
+            if (grp.pos() != pos) {
+                grp.pos(pos);
+                toUpdate.push(grp);
+            }
+            pos++;
+        }
+    );
+
+    if (toUpdate.length == 0) return;
+
+    var pcrud = new openils.PermaCrud({authtoken : user.authtoken});
+    pcrud.update(toUpdate); // run sync to prevent UI changes mid-update 
+}
+
+function newGroup() {
+
+    var dialog = new openils.widget.EditDialog({
+        fmClass : 'acplg',
+        mode : 'create',
+        parentNode : dojo.byId('acplg-edit-dialog'),
+        suppressFields : ['id'],
+        // note: when 'pos' is suppressed, the value is not propagated.
+        overrideWidgetArgs : {
+            pos : {widgetValue : groups.length, dijitArgs : {disabled : true}},
+            owner : {widgetValue : currentOrg, dijitArgs : {disabled : true}}
+        },
+        onPostSubmit : function(req, cudResults) {
+            if (cudResults && cudResults.length) {
+                // refresh the group display
+                drawGroupList(cudResults[0].id());
+            }
+        }
+    });
+
+    dialog.startup();
+    dialog.show();
+}
+
+function editGroup(grpId) {
+    var grp = groups.filter(function(g) { return g.id() == grpId })[0];
+
+    var dialog = new openils.widget.EditDialog({
+        fmObject : grp,
+        mode : 'update',
+        parentNode : dojo.byId('acplg-edit-dialog'),
+        suppressFields : ['id', 'pos', 'owner'],
+        onPostSubmit : function(req, cudResults) {
+            if (cudResults && cudResults.length) {
+                // refresh the group display
+                // pcrud.update returns ID only
+                drawGroupList(cudResults[0]);
+            }
+        }
+    });
+
+    dialog.startup();
+    dialog.show();
+}
+
+function deleteGroup(grpId) {
+    // confirm and delete
+    var pcrud = new openils.PermaCrud({authtoken : user.authtoken});
+    var grp = groups.filter(function(g) { return g.id() == grpId })[0];
+    pcrud.eliminate(grp, {oncomplete : function() { drawGroupList() }});
+}
+
+function drawGroupEntries(grpId) {
+    currentGroupId = grpId;
+
+    // init/reset the table of mapped copy locations
+    if (!locMapTbody) {
+        locMapTbody = dojo.byId('acplg-loc-map-tbody');
+        locMapRowTemplate = locMapTbody.removeChild(dojo.byId('acplg-loc-map-row'));
+    } else {
+        // clear out the previous table
+        while (node = locMapTbody.childNodes[0])
+            locMapTbody.removeChild(node);
+    }
+    
+    // update the 'selected' status
+    dojo.query('[group]').forEach(
+        function(node) {
+            if (node.getAttribute('group') == grpId) {
+                openils.Util.addCSSClass(node, 'acplg-group-selected');
+            } else {
+                openils.Util.removeCSSClass(node, 'acplg-group-selected');
+            }
+        }
+    );
+
+    currentGroupMaps = [];
+
+    // fetch the group
+    if (grpId) {
+        var pcrud = new openils.PermaCrud({authtoken : user.authtoken});
+        currentGroupMaps = pcrud.search('acplgm', {lgroup : grpId});
+    } 
+
+    // update the location selector to remove the already-selected orgs
+    drawLocations();
+
+    // draw the mapped copy locations
+    // remove any mapped locations from the location selector
+    dojo.forEach(currentGroupMaps,
+        function(map) {
+            var row = locMapRowTemplate.cloneNode(true);
+            row.setAttribute('map', map.id());
+            var loc = locations.filter(
+                function(loc) { return (loc.id() == map.location()) })[0];
+            dojo.query('[name=name]', row)[0].innerHTML = loc.name();
+            dojo.query('[name=owning_lib]', row)[0].innerHTML = 
+                fieldmapper.aou.findOrgUnit(loc.owning_lib()).shortname();
+            locMapTbody.appendChild(row);
+
+            // if the location is in the group, remove it from the location selection list
+            //removeLocationRow(loc.id());
+        }
+    );
+}
+
+function editLocations(action) {
+    var maps = [];
+    var tbody = (action == 'create') ? locTbody : locMapTbody;
+    dojo.forEach(tbody.getElementsByTagName('tr'),
+        function(row) {
+            var selector = dojo.query('[name=selector]', row)[0];
+            if (selector.checked) {
+                var map = new fieldmapper.acplgm();
+                map.lgroup(currentGroupId);
+                if (action == 'create') {
+                    map.location(row.getAttribute('location'));
+                } else {
+                    map.id(row.getAttribute('map'));
+                }
+                maps.push(map);
+            }
+        }
+    );
+
+    if (maps.length == 0) return;
+
+    // check for dupes
+    var pcrud = new openils.PermaCrud({authtoken : user.authtoken});
+    pcrud[action](maps, {
+        oncomplete : function() { 
+            drawGroupEntries(currentGroupId) 
+            /*
+            if (action != 'create') {
+                drawLocations();
+            }
+            */
+        }
+    });
+}
+
+function deSelectAll(node) {
+    dojo.query('[name=selector]', node).forEach(
+        function(selector) {
+            selector.checked = false;
+        }
+    );
+}
+
+/*
+function removeLocationRow(locId) {
+    var row = dojo.query('[location=' + locId + ']', locTbody)[0];
+    if (row) locTbody.removeChild(row);
+}
+*/
+
+openils.Util.addOnLoad(init);
diff --git a/Open-ILS/web/opac/locale/en-US/lang.dtd b/Open-ILS/web/opac/locale/en-US/lang.dtd
index 1fa3359..3561e30 100644
--- a/Open-ILS/web/opac/locale/en-US/lang.dtd
+++ b/Open-ILS/web/opac/locale/en-US/lang.dtd
@@ -715,6 +715,7 @@
 <!ENTITY staff.main.menu.admin.local_admin.patrons_due_refunds.accesskey "N">
 <!ENTITY staff.main.menu.admin.local_admin.address_alert.label "Address Alerts">
 <!ENTITY staff.main.menu.admin.local_admin.circ_limit_set.label "Circulation Limit Sets">
+<!ENTITY staff.main.menu.admin.local_admin.copy_location_group.label "Copy Location Groups">
 
 <!ENTITY staff.main.menu.admin.server_admin.label "Server Administration">
 <!ENTITY staff.main.menu.admin.server_admin.conify.org_unit_type.label "Organization Types">
diff --git a/Open-ILS/xul/staff_client/chrome/content/main/menu.js b/Open-ILS/xul/staff_client/chrome/content/main/menu.js
index b9f5a2a..d541553 100644
--- a/Open-ILS/xul/staff_client/chrome/content/main/menu.js
+++ b/Open-ILS/xul/staff_client/chrome/content/main/menu.js
@@ -1069,6 +1069,10 @@ main.menu.prototype = {
                 ['oncommand'],
                 function(event) { open_eg_web_page('conify/global/actor/address_alert', null, event); }
             ],
+            'cmd_local_admin_copy_location_group' : [
+                ['oncommand'],
+                function(event) { open_eg_web_page('conify/global/asset/copy_location_group', null, event); }
+            ],
             'cmd_acq_create_invoice' : [
                 ['oncommand'],
                 function(event) { open_eg_web_page('acq/invoice/view?create=1', 'menu.cmd_acq_create_invoice.tab', event); }
diff --git a/Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul b/Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul
index 220c91e..c71f8e1 100644
--- a/Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul
+++ b/Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul
@@ -153,6 +153,8 @@
     <command id="cmd_local_admin_circ_limit_set"
              perm="ADMIN_CIRC_MATRIX_MATCHPOINT VIEW_CIRC_MATRIX_MATCHPOINT"
              />
+    <command id="cmd_local_admin_copy_location_group"
+             perm="ADMIN_COPY_LOCATION_GROUP VIEW_COPY_LOCATION_GROUP" />
 
     <!-- server admin menu commands -->
     <command id="cmd_server_admin_org_type"
@@ -497,6 +499,7 @@
                 <menuitem label="&staff.server.admin.index.closed_dates;" command="cmd_local_admin_closed_dates"/>
                 <menuitem label="&staff.server.admin.index.copy_locations;" command="cmd_local_admin_copy_locations"/>
                 <menuitem label="&staff.main.menu.admin.local_admin.conify.copy_location_order.label;" command="cmd_local_admin_copy_location_order"/>
+                <menuitem label="&staff.main.menu.admin.local_admin.copy_location_group.label;" command="cmd_local_admin_copy_location_group"/>
                 <menuitem label="&staff.main.menu.admin.local_admin.copy_template.label;" accesskey="&staff.main.menu.admin.local_admin.copy_template.accesskey;" command="cmd_local_admin_copy_template"/>
                 <menuitem label="&staff.main.menu.admin.local_admin.conify.idl_field_doc.label;" command="cmd_local_admin_idl_field_doc"/>
                 <menuitem label="&staff.main.menu.admin.local_admin.conify.grp_penalty_threshold.label;" command="cmd_local_admin_grp_penalty_threshold"/>

commit 32458ad85b11e517eca78ebb3aead3300e31b4b2
Author: Bill Erickson <berick at esilibrary.com>
Date:   Fri Feb 17 11:57:19 2012 -0500

    Copy Location Search Groups : TPac org unit selector
    
    Adds support for viewing and searching on copy location groups in the
    tpac.  Groups appear within the org unit selector, when the selector is
    used in a search context.  Groups display below the owning org unit
    similar to a child org unit.  Groups are displayed for all org units
    that meet the following criteria:  search org unit, physical location,
    patron home org unit, plus ancestors and descendents of each.
    
    To support this, TPac gets a new "locg" CGI parameter, which contains
    the org unit and copy location group.  It takes the form
    org_id:group_id.  The TPac mod_perl code will extract this value and
    popuplate the search_ou accordingly.  For consistency, we also use
    ctx.search_ou instead of directly checking CGI.param('loc') within the
    template environment.
    
    This also includes a rewrite of the org_selector.tt2 template.  It
    changes it from a recursive routine to a depth-first while loop.  I did
    this mainly because the recursive approach was suffering from global
    variable clobbering in the template environment.  In theory, this new
    approach should be faster as well.
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Dan Scott <dan at coffeecode.net>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm
index b1ffd40..76b3f63 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm
@@ -251,10 +251,13 @@ sub load_common {
             return $self->load_logout($self->apache->unparsed_uri);
         }
     }
-    $ctx->{search_ou} = $self->_get_search_lib();
 
+    $self->extract_copy_location_group_info;
+    $ctx->{search_ou} = $self->_get_search_lib();
     $self->staff_saved_searches_set_expansion_state if $ctx->{is_staff};
     $self->load_eg_cache_hash;
+    $self->load_copy_location_groups;
+    $self->staff_saved_searches_set_expansion_state if $ctx->{is_staff};
 
     return Apache2::Const::OK;
 }
@@ -303,8 +306,6 @@ sub get_physical_loc {
     return $self->cgi->cookie(COOKIE_PHYSICAL_LOC);
 }
 
-
-
 # -----------------------------------------------------------------------------
 # Log in and redirect to the redirect_to URL (or home)
 # -----------------------------------------------------------------------------
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm
index b8907a6..ecf8ef0 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm
@@ -112,6 +112,10 @@ sub _prepare_biblio_search {
         $query .= " site($site)";
     }
 
+    if (my $grp = $ctx->{copy_location_group}) {
+        $query .= " location_groups($grp)";
+    }
+
     if(!$site) {
         ($site) = ($query =~ /site\(([^\)]+)\)/);
         $site ||= $ctx->{aou_tree}->()->shortname;
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm
index f10ccb7..e672eac 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm
@@ -280,28 +280,35 @@ sub fetch_marc_xml_by_id {
 
 sub _get_search_lib {
     my $self = shift;
+    my $ctx = $self->ctx;
+
+    # avoid duplicate lookups
+    return $ctx->{search_ou} if $ctx->{search_ou};
+
+    my $loc = $ctx->{copy_location_group_org};
+    return $loc if $loc;
 
     # loc param takes precedence
-    my $loc = $self->cgi->param('loc');
+    $loc = $self->cgi->param('loc');
     return $loc if $loc;
 
-    if ($self->ctx->{user}) {
+    if ($ctx->{user}) {
         # See if the user has a search library preference
         my $lset = $self->editor->search_actor_user_setting({
-            usr => $self->ctx->{user}->id, 
+            usr => $ctx->{user}->id, 
             name => 'opac.default_search_location'
         })->[0];
         return OpenSRF::Utils::JSON->JSON2perl($lset->value) if $lset;
 
         # Otherwise return the user's home library
-        return $self->ctx->{user}->home_ou;
+        return $ctx->{user}->home_ou;
     }
 
     if ($self->cgi->param('physical_loc')) {
         return $self->cgi->param('physical_loc');
     }
 
-    return $self->ctx->{aou_tree}->()->id;
+    return $ctx->{aou_tree}->()->id;
 }
 
 # This is defensively coded since we don't do much manual reading from the
@@ -340,4 +347,51 @@ sub load_eg_cache_hash {
     }
 }
 
+# Extracts the copy location org unit and group from the 
+# "logc" param, which takes the form org_id:grp_id.
+sub extract_copy_location_group_info {
+    my $self = shift;
+    my $ctx = $self->ctx;
+    if (my $clump = $self->cgi->param('locg')) {
+        my ($org, $grp) = split(/:/, $clump);
+        $ctx->{copy_location_group_org} = $org;
+        $ctx->{copy_location_group} = $grp if $grp;
+    }
+}
+
+sub load_copy_location_groups {
+    my $self = shift;
+    my $ctx = $self->ctx;
+
+    # User can access to the search location groups at the current 
+    # search lib, the physical location lib, and the patron's home ou.
+    my @ctx_orgs = $ctx->{search_ou};
+    push(@ctx_orgs, $ctx->{physical_loc}) if $ctx->{physical_loc};
+    push(@ctx_orgs, $ctx->{user}->home_ou) if $ctx->{user};
+
+    my $grps = $self->editor->search_asset_copy_location_group([
+        {
+            opac_visible => 't',
+            owner => {
+                in => {
+                    select => {aou => [{
+                        column => 'id', 
+                        transform => 'actor.org_unit_full_path',
+                        result_field => 'id',
+                    }]},
+                    from => 'aou',
+                    where => {id => \@ctx_orgs}
+                }
+            }
+        },
+        {order_by => {acplg => 'pos'}}
+    ]);
+
+    my %buckets;
+    push(@{$buckets{$_->owner}}, $_) for @$grps;
+    $ctx->{copy_location_groups} = \%buckets;
+}
+
+
+
 1;
diff --git a/Open-ILS/src/templates/opac/advanced.tt2 b/Open-ILS/src/templates/opac/advanced.tt2
index 17cb74f..220c56f 100644
--- a/Open-ILS/src/templates/opac/advanced.tt2
+++ b/Open-ILS/src/templates/opac/advanced.tt2
@@ -1,9 +1,9 @@
-[%-  PROCESS "opac/parts/header.tt2";
+[%- PROCESS "opac/parts/header.tt2";
     WRAPPER "opac/parts/base.tt2";
     INCLUDE "opac/parts/topnav.tt2";
     ctx.page_title = l("Advanced Search");
     pane = CGI.param("pane") || "advanced";
-    loc = CGI.param("loc");
+    loc = ctx.search_ou;
 -%]
     <div id="search-wrapper">
         <div id="search-box">
diff --git a/Open-ILS/src/templates/opac/myopac/prefs_notify.tt2 b/Open-ILS/src/templates/opac/myopac/prefs_notify.tt2
index 7d23021..aeaaa41 100644
--- a/Open-ILS/src/templates/opac/myopac/prefs_notify.tt2
+++ b/Open-ILS/src/templates/opac/myopac/prefs_notify.tt2
@@ -51,7 +51,7 @@
                         [% IF ctx.user_setting_map.$setting; %] value='[% ctx.user_setting_map.$setting | html %]' [% END %]/>
                 </td>
             </tr>
-            [% IF ctx.get_org_setting(CGI.param('loc') OR ctx.aou_tree.id, 'sms.enable') == 1 %]
+            [% IF ctx.get_org_setting(ctx.search_ou, 'sms.enable') == 1 %]
             <tr>
                 <td>[% l('Notify by Text by default when a hold is ready for pickup?') %]</td>
                 <td>
diff --git a/Open-ILS/src/templates/opac/parts/advanced/expert.tt2 b/Open-ILS/src/templates/opac/parts/advanced/expert.tt2
index 24fbd1c..da8bba6 100644
--- a/Open-ILS/src/templates/opac/parts/advanced/expert.tt2
+++ b/Open-ILS/src/templates/opac/parts/advanced/expert.tt2
@@ -1,4 +1,3 @@
-[% loc = CGI.param("loc") %]
 <form action="[% ctx.opac_root %]/results" method="GET">
     <div class="header_middle">[% l("Expert Search") %]</div>
     <input type="hidden" name="_special" value="1" />
diff --git a/Open-ILS/src/templates/opac/parts/advanced/search.tt2 b/Open-ILS/src/templates/opac/parts/advanced/search.tt2
index f14aaa8..6b62221 100644
--- a/Open-ILS/src/templates/opac/parts/advanced/search.tt2
+++ b/Open-ILS/src/templates/opac/parts/advanced/search.tt2
@@ -67,7 +67,7 @@
                         <td valign='top'>
                             <strong>[% l("Search Library") %]</strong><br />
                             [% PROCESS "opac/parts/org_selector.tt2";
-                                PROCESS build_org_selector name='loc' value=ctx.search_ou %]
+                                PROCESS build_org_selector show_loc_groups=1 %]
                             <div style="position:relative;top:7px;">
                                 <input type='checkbox' name="modifier"
                                     value="available"[% CGI.param('modifier').grep('available').size ? ' checked="checked"' : '' %]
diff --git a/Open-ILS/src/templates/opac/parts/org_selector.tt2 b/Open-ILS/src/templates/opac/parts/org_selector.tt2
index 96bdcbf..a2c222e 100644
--- a/Open-ILS/src/templates/opac/parts/org_selector.tt2
+++ b/Open-ILS/src/templates/opac/parts/org_selector.tt2
@@ -1,33 +1,66 @@
 [%
-    BLOCK build_org_selector_options;
-        disabled = '';
-        selected = '';
-        IF can_have_vols_only AND walker.ou_type.can_have_vols != 't';
-            disabled = 'disabled="disabled"';
-        ELSIF walker.id == value;
-            selected = 'selected="selected"';
-        END;
-        IF ctx.is_staff || walker.opac_visible == 't';
-%]
-            <option value='[% walker.id | uri %]' [% selected %] [% disabled %]>
-                [%
-                    pad = walker.ou_type.depth * 2;
-                    FOR idx IN [0..pad]; '&nbsp;'; END;
-                    walker.name | html;
-                %]
-            </option>
-            [% FOR child IN walker.children;
-                PROCESS build_org_selector_options walker=child value=value;
-            END;
+# Org Unit Selector Widget :
+#   PROCESS build_org_selector id='selector-id' name='selector-name' 
+#       value=org_id show_loc_groups=1/0 can_have_vols_only=1/0
+
+BLOCK build_org_selector;
+    node_stack = [{org => org_unit || ctx.aou_tree}];
+    IF !name; 
+        name = 'loc';
+        IF show_loc_groups; name = 'locg'; END;
+    END;
+    IF !value;
+        value = ctx.search_ou;
+        IF show_loc_groups; 
+            value = CGI.param('locg') || ctx.search_ou;
         END;
     END;
+    %]
 
-    # XXX TODO probably put this BLOCK somewhere else so it can be used widely.
-    # Org Unit Selector Widget :
-    #   PROCESS build_org_selector id='selector-id' name='selector-name'
-    BLOCK build_org_selector;
-%]
     <select [% IF id %] id='[% id %]' [% END %] name='[% name %]'>
-    [% PROCESS build_org_selector_options walker=(org_unit || ctx.aou_tree) value=value %]
+    [% 
+        WHILE node_stack.size > 0; 
+            node = node_stack.pop();
+            org_unit = node.org;
+            loc_grp = node.loc_grp;
+            ou_id = org_unit.id;
+            disabled = '';
+            selected = '';
+
+            NEXT UNLESS ctx.is_staff || org_unit.opac_visible == 't';
+
+            IF !loc_grp;
+                IF show_loc_groups;
+                    FOR grp IN ctx.copy_location_groups.$ou_id.reverse;
+                        node_stack.push({org => org_unit, loc_grp => grp});
+                    END;
+                END;
+                FOR child IN org_unit.children.reverse;
+                    node_stack.push({org => child});
+                END;
+            END;
+
+            node_value = ou_id;
+            IF loc_grp; node_value = node_value _ ':' _ loc_grp.id; END;
+
+            IF can_have_vols_only AND org_unit.ou_type.can_have_vols != 't';
+                disabled = 'disabled="disabled"';
+            ELSIF node_value == value;
+                selected = 'selected="selected"';
+            END %] 
+
+            <option value='[% node_value %]' [% selected %] [% disabled %]> 
+            [%
+                # loc_grp's are displayed as children of the current org
+                depth = org_unit.ou_type.depth;
+                IF loc_grp; depth = depth + 1; END;
+                pad = depth * 2;
+                FOR idx IN [0..pad]; '&nbsp;'; END;
+                loc_grp ? loc_grp.name : org_unit.name | html ;
+            %]
+            </option> 
+            [%
+        END;
+    %]
     </select>
-[%  END %]
+[% END %]
diff --git a/Open-ILS/src/templates/opac/parts/place_hold.tt2 b/Open-ILS/src/templates/opac/parts/place_hold.tt2
index bd95814..97ffe96 100644
--- a/Open-ILS/src/templates/opac/parts/place_hold.tt2
+++ b/Open-ILS/src/templates/opac/parts/place_hold.tt2
@@ -83,7 +83,7 @@
                     [% l('Phone Number:') %]<input type="text" name="phone_notify" [% setting = 'opac.default_phone';
                     IF ctx.user_setting_map.$setting; %] value='[% ctx.user_setting_map.$setting | html %]' [% END %]/>
                 </blockquote>
-                [% IF ctx.get_org_setting(CGI.param('loc') OR ctx.aou_tree.id, 'sms.enable') == 1 %]
+                [% IF ctx.get_org_setting(ctx.search_ou, 'sms.enable') == 1 %]
                 <input type="checkbox" name="sms_notify_checkbox"
                     [% IF ctx.default_sms_notify %]checked="checked"[% END %]/>
                     [% l('Yes, by Text Messaging') %]<br/>
diff --git a/Open-ILS/src/templates/opac/parts/preserve_params.tt2 b/Open-ILS/src/templates/opac/parts/preserve_params.tt2
index 14fe5be..3517f58 100644
--- a/Open-ILS/src/templates/opac/parts/preserve_params.tt2
+++ b/Open-ILS/src/templates/opac/parts/preserve_params.tt2
@@ -1,6 +1,6 @@
 [%- 
 UNLESS params;
-    params = ['loc', 'query', 'qtype', 'sort'];
+    params = ['locg', 'loc', 'query', 'qtype', 'sort'];
 END;
 FOR param IN params;
     IF CGI.param(param); %]
diff --git a/Open-ILS/src/templates/opac/parts/record/copy_counts.tt2 b/Open-ILS/src/templates/opac/parts/record/copy_counts.tt2
index 9d85ae7..defa0ae 100644
--- a/Open-ILS/src/templates/opac/parts/record/copy_counts.tt2
+++ b/Open-ILS/src/templates/opac/parts/record/copy_counts.tt2
@@ -10,7 +10,7 @@
     <li>
     [% l('[quant,_1,copy,copies] at [_2].', ou_avail, ctx.get_aou(ou_id).name)
         | html %]
-    [%- IF ou_avail > 0 && ou_id != CGI.param('loc'); %]
+    [%- IF ou_avail > 0 && ou_id != ctx.search_ou; %]
     <a href="[% mkurl('', {loc => ou_id}); %]"
        title="[% l('Show copies at [_1]', ctx.get_aou(ou_id).name); %]">
        [%- l('(Show)'); %]</a>
diff --git a/Open-ILS/src/templates/opac/parts/record/copy_table.tt2 b/Open-ILS/src/templates/opac/parts/record/copy_table.tt2
index 8148dd9..240f970 100644
--- a/Open-ILS/src/templates/opac/parts/record/copy_table.tt2
+++ b/Open-ILS/src/templates/opac/parts/record/copy_table.tt2
@@ -54,7 +54,7 @@ END;
                 org_name | html
             -%]
             </td>
-            <td header='copy_header_callnumber'>[% callnum | html %] [% IF ctx.get_org_setting(CGI.param('loc') OR ctx.aou_tree.id, 'sms.enable') == 1 %](<a href="[% mkurl(ctx.opac_root _ '/sms_cn', {copy_id => copy_info.id}) %]">Text</a>)[% END %]</td>
+            <td header='copy_header_callnumber'>[% callnum | html %] [% IF ctx.get_org_setting(ctx.search_ou, 'sms.enable') == 1 %](<a href="[% mkurl(ctx.opac_root _ '/sms_cn', {copy_id => copy_info.id}) %]">Text</a>)[% END %]</td>
             [%- IF has_parts == 'true' %]
             <td header='copy_header_part'>[% copy_info.part_label | html %]</td>
             [%- END %]
diff --git a/Open-ILS/src/templates/opac/parts/record/refworks.tt2 b/Open-ILS/src/templates/opac/parts/record/refworks.tt2
index 928ce90..ed02a74 100644
--- a/Open-ILS/src/templates/opac/parts/record/refworks.tt2
+++ b/Open-ILS/src/templates/opac/parts/record/refworks.tt2
@@ -1,9 +1,6 @@
 [%
     # Default to the root of the org unit tree in the absence of a specific library
-    loc = ctx.aou_tree.id;
-    IF CGI.param('loc');
-        loc = CGI.param('loc');
-    END;
+    loc = ctx.search_ou;
 
     # Get the full name of the library
     ou_name = ctx.get_aou(loc).name | uri;
diff --git a/Open-ILS/src/templates/opac/parts/record/series.tt2 b/Open-ILS/src/templates/opac/parts/record/series.tt2
index 06def16..4695768 100644
--- a/Open-ILS/src/templates/opac/parts/record/series.tt2
+++ b/Open-ILS/src/templates/opac/parts/record/series.tt2
@@ -1,6 +1,6 @@
 [% 
     series_tags = ['440', '490', '800', '810', '811', '830', '694']; 
-    loc = CGI.param('loc');
+    loc = ctx.search_ou;
 %]
 
 [% BLOCK render_series;
diff --git a/Open-ILS/src/templates/opac/parts/record/subjects.tt2 b/Open-ILS/src/templates/opac/parts/record/subjects.tt2
index a78bdef..59e26a5 100644
--- a/Open-ILS/src/templates/opac/parts/record/subjects.tt2
+++ b/Open-ILS/src/templates/opac/parts/record/subjects.tt2
@@ -28,7 +28,6 @@
     ];
 
     BLOCK render_subject;
-        loc = CGI.param('loc') | uri;
         xpath = xpath || '//*[starts-with(@tag,"6")]';
         FOR node IN ctx.marc_xml.findnodes(xpath);
             all_terms = [];
diff --git a/Open-ILS/src/templates/opac/parts/searchbar.tt2 b/Open-ILS/src/templates/opac/parts/searchbar.tt2
index 3c12bd9..d0fa8ea 100644
--- a/Open-ILS/src/templates/opac/parts/searchbar.tt2
+++ b/Open-ILS/src/templates/opac/parts/searchbar.tt2
@@ -28,7 +28,7 @@
                 [%- END # autosuggest enabled %] />
         </span>
         [%- INCLUDE "opac/parts/qtype_selector.tt2" id="qtype";
-            l(' in '); PROCESS build_org_selector name='loc' value=ctx.search_ou;
+            l(' in '); PROCESS build_org_selector show_loc_groups=1
     %]
     <span>
         <input id='search-submit-go' type="submit" value="[% l('Search') %]" alt="[% l('Search') %]" class="opac-button"

commit fb1a10e370bf708b1f4fb1c59dc029ed75a85d94
Author: Bill Erickson <berick at esilibrary.com>
Date:   Wed Feb 15 15:33:29 2012 -0500

    Copy Location Search Groups : location_groups() QP filter
    
    Adds a new QueryParser search filter "location_groups" which takes a
    list of asset.copy_location_group IDs and filters on the mapped copy
    locations.
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Dan Scott <dan at coffeecode.net>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Driver/Pg/QueryParser.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Driver/Pg/QueryParser.pm
index c38f574..f88affe 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Driver/Pg/QueryParser.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Driver/Pg/QueryParser.pm
@@ -422,6 +422,7 @@ __PACKAGE__->add_search_filter( 'during' );
 # used by layers above this
 __PACKAGE__->add_search_filter( 'statuses' );
 __PACKAGE__->add_search_filter( 'locations' );
+__PACKAGE__->add_search_filter( 'location_groups' );
 __PACKAGE__->add_search_filter( 'site' );
 __PACKAGE__->add_search_filter( 'lasso' );
 __PACKAGE__->add_search_filter( 'my_lasso' );
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/metabib.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/metabib.pm
index a5cb550..eb2602a 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/metabib.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/metabib.pm
@@ -2992,6 +2992,22 @@ sub query_parser_fts {
         @location = @{$filter->args} if (@{$filter->args});
     }
 
+    # gather location_groups
+    if (my ($filter) = $query->parse_tree->find_filter('location_groups')) {
+        my @loc_groups = @{$filter->args} if (@{$filter->args});
+        
+        # collect the mapped locations and add them to the locations() filter
+        if (@loc_groups) {
+
+            my $cstore = OpenSRF::AppSession->create( 'open-ils.cstore' );
+            my $maps = $cstore->request(
+                'open-ils.cstore.direct.asset.copy_location_group_map.search.atomic',
+                {lgroup => \@loc_groups})->gather(1);
+
+            push(@location, $_->location) for @$maps;
+        }
+    }
+
 
     my $param_check = $limit || $query->superpage_size || 'NULL';
     my $param_offset = $offset || 'NULL';
@@ -3183,7 +3199,7 @@ sub query_parser_fts_wrapper {
         if ( ref($args{between}) and @{$args{between}} == 2 and $args{between}[0] =~ /^\d+$/ and $args{between}[1] =~ /^\d+$/ );
 
 
-	my (@between, at statuses, at locations, at types, at forms, at lang, at aud, at lit_form, at vformats, at bib_level);
+	my (@between, at statuses, at locations, at location_groups, at types, at forms, at lang, at aud, at lit_form, at vformats, at bib_level);
 
 	# XXX legacy format and item type support
 	if ($args{format}) {
@@ -3192,7 +3208,7 @@ sub query_parser_fts_wrapper {
 		$args{item_form} = [ split '', $f ];
 	}
 
-    for my $filter ( qw/locations statuses between audience language lit_form item_form item_type bib_level vr_format/ ) {
+    for my $filter ( qw/locations location_groups statuses between audience language lit_form item_form item_type bib_level vr_format/ ) {
     	if (my $s = $args{$filter}) {
     		$s = [$s] if (!ref($s));
 

commit c9dae1827b4e6d01fa78b10343a9530aeca3f824
Author: Bill Erickson <berick at esilibrary.com>
Date:   Wed Feb 15 12:22:47 2012 -0500

    Copy Location Search Groups : DB / IDL
    
    Adds 2 new tables, one for defining copy location groups and another for
    mapping copy locations to groups.
    
    Signed-off-by: Bill Erickson <berick at esilibrary.com>
    Signed-off-by: Dan Scott <dan at coffeecode.net>

diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml
index 2924bfc..c7bb14d 100644
--- a/Open-ILS/examples/fm_IDL.xml
+++ b/Open-ILS/examples/fm_IDL.xml
@@ -3928,6 +3928,53 @@ SELECT  usr,
             </actions>
         </permacrud>
 	</class>
+	<class id="acplg" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="asset::copy_location_group" oils_persist:tablename="asset.copy_location_group" reporter:label="Copy/Shelving Location Group">
+		<fields oils_persist:primary="id" oils_persist:sequence="asset.copy_location_group_id_seq">
+			<field reporter:label="ID" name="id" reporter:selector="name" reporter:datatype="id"/>
+			<field reporter:label="Name" name="name"  reporter:datatype="text" oils_persist:i18n="true"/>
+			<field reporter:label="Is OPAC Visible?" name="opac_visible" reporter:datatype="bool"/>
+			<field reporter:label="Owning Org Unit" name="owner"  reporter:datatype="org_unit"/>
+            <field reporter:label="Position" name="pos" reporter:datatype="int"/>
+            <field reporter:label="Copy Location Mappings" name="location_maps" oils_persist:virtual="true" reporter:datatype="link"/>
+		</fields>
+		<links>
+			<link field="owner" reltype="has_a" key="id" map="" class="aou"/>
+			<link field="location_maps" reltype="has_many" key="lgroup" map="" class="acplgm"/>
+		</links>
+        <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+            <actions>
+                <create permission="ADMIN_COPY_LOCATION_GROUP" context_field="owner"/>
+                <retrieve/>
+                <update permission="ADMIN_COPY_LOCATION_GROUP" context_field="owner"/>
+                <delete permission="ADMIN_COPY_LOCATION_GROUP" context_field="owner"/>
+            </actions>
+        </permacrud>
+	</class>
+	<class id="acplgm" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="asset::copy_location_group_map" oils_persist:tablename="asset.copy_location_group_map" reporter:label="Copy/Shelving Location Group Map">
+		<fields oils_persist:primary="id" oils_persist:sequence="asset.copy_location_group_map_id_seq">
+			<field reporter:label="ID" name="id" reporter:selector="name" reporter:datatype="id"/>
+            <field reporter:label="Group" name="lgroup" reporter:datatype="link"/>
+            <field reporter:label="Copy Location" name="location" reporter:datatype="link"/>
+		</fields>
+		<links>
+			<link field="lgroup" reltype="has_a" key="id" map="" class="acplg"/>
+			<link field="location" reltype="has_a" key="id" map="" class="acpl"/>
+		</links>
+        <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+            <actions>
+                <create permission="ADMIN_COPY_LOCATION_GROUP">
+					<context link="lgroup" field="owner" />
+                </create>
+                <retrieve/>
+                <update permission="ADMIN_COPY_LOCATION_GROUP">
+					<context link="lgroup" field="owner" />
+                </update>
+                <delete permission="ADMIN_COPY_LOCATION_GROUP">
+					<context link="lgroup" field="owner" />
+                </delete>
+            </actions>
+        </permacrud>
+	</class>
 
     <class id="acplo" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="asset::copy_location_order" oils_persist:tablename="asset.copy_location_order" reporter:label="Copy/Shelving Location Order">
         <fields oils_persist:primary="id" oils_persist:sequence="asset.copy_location_order_id_seq">
diff --git a/Open-ILS/src/sql/Pg/040.schema.asset.sql b/Open-ILS/src/sql/Pg/040.schema.asset.sql
index 92fd81c..85d58f6 100644
--- a/Open-ILS/src/sql/Pg/040.schema.asset.sql
+++ b/Open-ILS/src/sql/Pg/040.schema.asset.sql
@@ -50,6 +50,23 @@ CREATE TABLE asset.copy_location_order
         CONSTRAINT acplo_once_per_org UNIQUE ( location, org )
 );
 
+CREATE TABLE asset.copy_location_group (
+    id              SERIAL  PRIMARY KEY,
+    name            TEXT    NOT NULL, -- i18n
+    owner           INT     NOT NULL REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    pos             INT     NOT NULL DEFAULT 0,
+    opac_visible    BOOL    NOT NULL DEFAULT TRUE,
+    CONSTRAINT lgroup_once_per_owner UNIQUE (owner,name)
+);
+
+CREATE TABLE asset.copy_location_group_map (
+    id       SERIAL PRIMARY KEY,
+    location    INT     NOT NULL REFERENCES asset.copy_location (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    lgroup      INT     NOT NULL REFERENCES asset.copy_location_group (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    CONSTRAINT  lgroup_once_per_group UNIQUE (lgroup,location)
+);
+
+
 CREATE TABLE asset.copy (
 	id		BIGSERIAL			PRIMARY KEY,
 	circ_lib	INT				NOT NULL REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_loc_search_groups.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_loc_search_groups.sql
new file mode 100644
index 0000000..a6dd74a
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_loc_search_groups.sql
@@ -0,0 +1,29 @@
+BEGIN;
+
+-- SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+
+CREATE TABLE asset.copy_location_group (
+    id              SERIAL  PRIMARY KEY,
+    name            TEXT    NOT NULL, -- i18n
+    owner           INT     NOT NULL REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    pos             INT     NOT NULL DEFAULT 0,
+    opac_visible    BOOL    NOT NULL DEFAULT TRUE,
+    CONSTRAINT lgroup_once_per_owner UNIQUE (owner,name)
+);
+
+CREATE TABLE asset.copy_location_group_map (
+    id       SERIAL PRIMARY KEY,
+    location    INT     NOT NULL REFERENCES asset.copy_location (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    lgroup      INT     NOT NULL REFERENCES asset.copy_location_group (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    CONSTRAINT  lgroup_once_per_group UNIQUE (lgroup,location)
+);
+
+COMMIT;
+
+/* UNDO
+BEGIN;
+DROP TABLE asset.copy_location_group_map;
+DROP TABLE asset.copy_location_group;
+COMMIT;
+*/
+

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

Summary of changes:
 Open-ILS/examples/fm_IDL.xml                       |   48 +++
 .../Application/Storage/Driver/Pg/QueryParser.pm   |    1 +
 .../Application/Storage/Publisher/metabib.pm       |   20 +-
 .../src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm    |    7 +-
 .../perlmods/lib/OpenILS/WWW/EGCatLoader/Record.pm |   18 +-
 .../perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm |    4 +
 .../perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm   |   64 ++++-
 Open-ILS/src/sql/Pg/040.schema.asset.sql           |   18 +
 .../upgrade/XXXX.schema.copy_loc_search_groups.sql |   30 ++
 .../conify/global/asset/copy_location_group.tt2    |  141 ++++++++
 Open-ILS/src/templates/opac/advanced.tt2           |    4 +-
 .../src/templates/opac/myopac/prefs_notify.tt2     |    2 +-
 .../src/templates/opac/parts/advanced/expert.tt2   |    1 -
 .../src/templates/opac/parts/advanced/search.tt2   |    2 +-
 Open-ILS/src/templates/opac/parts/org_selector.tt2 |  108 +++++--
 Open-ILS/src/templates/opac/parts/place_hold.tt2   |    2 +-
 .../src/templates/opac/parts/preserve_params.tt2   |    2 +-
 .../templates/opac/parts/record/copy_counts.tt2    |    8 +-
 .../src/templates/opac/parts/record/copy_table.tt2 |    2 +-
 .../src/templates/opac/parts/record/refworks.tt2   |    5 +-
 .../src/templates/opac/parts/record/series.tt2     |    2 +-
 .../src/templates/opac/parts/record/subjects.tt2   |    1 -
 Open-ILS/src/templates/opac/parts/searchbar.tt2    |    2 +-
 .../conify/global/asset/copy_location_group.js     |  360 ++++++++++++++++++++
 Open-ILS/web/opac/locale/en-US/lang.dtd            |    1 +
 .../xul/staff_client/chrome/content/main/menu.js   |    4 +
 .../chrome/content/main/menu_frame_menus.xul       |    3 +
 docs/RELEASE_NOTES_2_2.txt                         |   16 +
 28 files changed, 815 insertions(+), 61 deletions(-)
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_loc_search_groups.sql
 create mode 100644 Open-ILS/src/templates/conify/global/asset/copy_location_group.tt2
 create mode 100644 Open-ILS/web/js/ui/default/conify/global/asset/copy_location_group.js


hooks/post-receive
-- 
Evergreen ILS


More information about the open-ils-commits mailing list