LP#1815815: Library Groups
authorMike Rylander <mrylander@gmail.com>
Mon, 28 Jan 2019 21:02:43 +0000 (16:02 -0500)
committerGalen Charlton <gmc@equinoxinitiative.org>
Thu, 11 Mar 2021 18:18:24 +0000 (13:18 -0500)
This branch implements Library Groups (what used to be called "lassos") for
Evergreen.

Evergreen has, internally, a concept called "lassos" that allows an
administrator to define a group of org units to search that has no relation
to the hierarchical org tree. For instance, one might create a group of law
or science libraries within a university consortium, or group all school
libraries together.

In addition to the previous always-visible type of Library Group (lasso), one
can now make them context-aware so that that only show up if the current
search location is included as one of the org units in the Library Group.
This is implemented without regard to the org unit hierarchy, and so requires
that the relevant ancestor and descendent org units be included in the group
along with those that actually hold copies, but allows for complete
flexibility in context-aware Library Group configuration.

Signed-off-by: Mike Rylander <mrylander@gmail.com>
Signed-off-by: Ruth Frasur <rfrasur@library.in.gov>
Signed-off-by: Terran McCanna <tmccanna@georgialibraries.org>
Signed-off-by: Galen Charlton <gmc@equinoxinitiative.org>
16 files changed:
Open-ILS/examples/fm_IDL.xml
Open-ILS/src/eg2/src/app/staff/admin/server/admin-server-splash.component.html
Open-ILS/src/perlmods/lib/OpenILS/Application/AppUtils.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Driver/Pg/QueryParser.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/metabib.pm
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm
Open-ILS/src/sql/Pg/005.schema.actors.sql
Open-ILS/src/sql/Pg/950.data.seed-values.sql
Open-ILS/src/sql/Pg/upgrade/XXXX.schema.lassos.sql [new file with mode: 0644]
Open-ILS/src/templates/opac/parts/advanced/search.tt2
Open-ILS/src/templates/opac/parts/config.tt2
Open-ILS/src/templates/opac/parts/org_selector.tt2
Open-ILS/src/templates/opac/parts/searchbar.tt2
Open-ILS/src/templates/staff/admin/server/t_splash.tt2

index 36dc6d5..45ce731 100644 (file)
@@ -6369,26 +6369,30 @@ SELECT  usr,
             </actions>
         </permacrud>
        </class>
-       <class id="lasso" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="actor::org_lasso" oils_persist:tablename="actor.org_lasso" reporter:label="Org Lasso">
+       <class id="lasso" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="actor::org_lasso" oils_persist:tablename="actor.org_lasso" reporter:label="Library Group">
                <fields oils_persist:primary="id" oils_persist:sequence="actor.org_lasso_id_seq">
-                       <field name="id" reporter:datatype="id" />
-                       <field name="name" reporter:datatype="text"/>
+                       <field reporter:label="Library Group ID" name="id" reporter:datatype="id" reporter:selector="name"/>
+                       <field reporter:label="Name" name="name" reporter:datatype="text"/>
+                       <field reporter:label="Global?" name="global" reporter:datatype="bool"/>
+                       <field reporter:label="Maps" name="maps" oils_persist:virtual="true" reporter:datatype="link"/>
                </fields>
-               <links/>
+               <links>
+                       <link field="maps" reltype="has_many" key="lasso" map="" class="lmap"/>
+        </links>
         <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
             <actions>
-                <create permission="CREATE_LASSO" global_required="true"/>
-                <retrieve permission="CREATE_LASSO UPDATE_LASSO DELETE_LASSO" global_required="true"/>
-                <update permission="UPDATE_LASSO" global_required="true"/>
-                <delete permission="DELETE_LASSO" global_required="true"/>
+                <create permission="ADMIN_LIBRARY_GROUPS" global_required="true"/>
+                <retrieve/>
+                <update permission="ADMIN_LIBRARY_GROUPS" global_required="true"/>
+                <delete permission="ADMIN_LIBRARY_GROUPS" global_required="true"/>
             </actions>
         </permacrud>
        </class>
-       <class id="lmap" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="actor::org_lasso_map" oils_persist:tablename="actor.org_lasso_map" reporter:label="Org Lasso Map">
+       <class id="lmap" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="actor::org_lasso_map" oils_persist:tablename="actor.org_lasso_map" reporter:label="Library Group Map">
                <fields oils_persist:primary="id" oils_persist:sequence="actor.org_lasso_map_id_seq">
-                       <field name="id" reporter:datatype="id" />
-                       <field name="lasso" reporter:datatype="link"/>
-                       <field name="org_unit" reporter:datatype="org_unit"/>
+                       <field reporter:label="Library Group Map ID" name="id" reporter:datatype="id" />
+                       <field reporter:label="Library Group" name="lasso" reporter:datatype="link"/>
+                       <field reporter:label="Organizational Unit" name="org_unit" reporter:datatype="org_unit"/>
                </fields>
                <links>
                        <link field="lasso" reltype="has_a" key="id" map="" class="lasso"/>
@@ -6396,10 +6400,10 @@ SELECT  usr,
                </links>
         <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
             <actions>
-                <create permission="CREATE_LASSO_MAP" global_required="true"/>
-                <retrieve permission="CREATE_LASSO_MAP UPDATE_LASSO_MAP DELETE_LASSO_MAP" global_required="true"/>
-                <update permission="UPDATE_LASSO_MAP" global_required="true"/>
-                <delete permission="DELETE_LASSO_MAP" global_required="true"/>
+                <create permission="ADMIN_LIBRARY_GROUPS" global_required="true"/>
+                <retrieve/>
+                <update permission="ADMIN_LIBRARY_GROUPS" global_required="true"/>
+                <delete permission="ADMIN_LIBRARY_GROUPS" global_required="true"/>
             </actions>
         </permacrud>
        </class>
index de399d6..fd65e16 100644 (file)
       routerLink="/staff/admin/server/config/copy_status"></eg-link-table-link>
     <eg-link-table-link i18n-label label="Item Tag Types"  
       routerLink="/staff/admin/server/config/copy_tag_type"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Library Groups"  
+      routerLink="/staff/admin/server/actor/org_lasso"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Library Group Maps"  
+      routerLink="/staff/admin/server/actor/org_lasso_map"></eg-link-table-link>
     <eg-link-table-link i18n-label label="MARC Coded Value Maps"  
       routerLink="/staff/admin/server/config/coded_value_map"></eg-link-table-link>
     <eg-link-table-link i18n-label label="MARC Import Remove Fields"  
index 9190ad8..76ee00d 100644 (file)
@@ -775,6 +775,20 @@ sub find_org_by_shortname {
     return undef;
 }
 
+sub find_lasso_by_name {
+    my( $self, $name )  = @_;
+    return $self->simplereq(
+        'open-ils.cstore', 
+        'open-ils.cstore.direct.actor.org_lasso.search.atomic', { name => $name } )->[0];
+}
+
+sub fetch_lasso_org_maps {
+    my( $self, $lasso )  = @_;
+    return $self->simplereq(
+        'open-ils.cstore', 
+        'open-ils.cstore.direct.actor.org_lasso_map.search.atomic', { lasso => $lasso } );
+}
+
 sub fetch_non_cat_type_by_name_and_org {
     my( $self, $name, $orgId ) = @_;
     $logger->debug("Fetching non cat type $name at org $orgId");
index 950a081..c4e3455 100644 (file)
@@ -1450,15 +1450,34 @@ sub flatten {
     my $site_org = $ot;
     my $negate = 'FALSE';
 
-    my ($site_filter) = grep { $_->name eq 'site' } @{$self->filters};
-    if ($site_filter and @{$site_filter->args} == 1) {
-       $negate = $site_filter->negate ? 'TRUE' : 'FALSE';
+    my @lasso_list;
+    my ($lasso_filter) = grep { $_->name eq 'lasso' } @{$self->filters};
+    if ($lasso_filter and @{$lasso_filter->args} == 1) {
+        $negate = $lasso_filter->negate ? 'TRUE' : 'FALSE';
+
+        my $lasso = $lasso_filter->args->[0];
+        if ($lasso !~ /^\d+$/) {
+            $lasso = $U->find_lasso_by_name($lasso);
+        }
+
+        if ($lasso) {
+            $lasso = $lasso->id if (ref $lasso);
+            @lasso_list = map { $_->org_unit } @{ $U->fetch_lasso_org_maps($lasso) };
+        }
+    }
 
-       my $sitename = $site_filter->args->[0];
-       $site_org = $U->find_org_by_shortname($ot, $sitename) || $ot;
+
+    if (!@lasso_list) {
+        my ($site_filter) = grep { $_->name eq 'site' } @{$self->filters};
+        if ($site_filter and @{$site_filter->args} == 1) {
+           $negate = $site_filter->negate ? 'TRUE' : 'FALSE';
+
+           my $sitename = $site_filter->args->[0];
+           $site_org = $U->find_org_by_shortname($ot, $sitename) || $ot;
+        }
     }
 
-    my $dorgs = $U->get_org_descendants($site_org->id, $depth_filter);
+    my $dorgs = scalar(@lasso_list) ? [@lasso_list] : $U->get_org_descendants($site_org->id, $depth_filter);
     my $aorgs = $U->get_org_ancestors($site_org->id);
 
     flesh_parents($ot);
@@ -1472,7 +1491,8 @@ sub flatten {
     push @{$vis_filter{'c_attr'}},
         "search.calculate_visibility_attribute_test('circ_lib','{".join(',', @$dorgs)."}',$negate)";
 
-    if (!$self->find_filter('locations') && !$self->find_filter('location_groups')) {
+    # NOTE: both lassos and shelving locations preclude Located URI search
+    if (!$lasso_filter && !$self->find_filter('locations') && !$self->find_filter('location_groups')) {
         my $lorgs = [@$aorgs];
         my $luri_as_copy_gf = $U->get_global_flag('opac.located_uri.act_as_copy');
         push @$lorgs, @$dorgs if ($luri_as_copy_gf and $U->is_true($luri_as_copy_gf->enabled));
index c0dc2ac..987b906 100644 (file)
@@ -2972,30 +2972,35 @@ sub query_parser_fts {
     }
     $ou = actor::org_unit->search( { shortname => $ou } )->next->id if ($ou and $ou !~ /^(-)?\d+$/);
 
-    # gather lasso, as with $ou
-    my $lasso = $args{lasso};
-    if (my ($filter) = $query->parse_tree->find_filter('lasso')) {
-            $lasso = $filter->args->[0] if (@{$filter->args});
-    }
-    $lasso = actor::org_lasso->search( { name => $lasso } )->next->id if ($lasso and $lasso !~ /^\d+$/);
-    $lasso = -$lasso if ($lasso);
-
 
-#    # XXX once we have org_unit containers, we can make user-defined lassos .. WHEEE
-#    # gather user lasso, as with $ou and lasso
-#    my $mylasso = $args{my_lasso};
-#    if (my ($filter) = $query->parse_tree->find_filter('my_lasso')) {
-#            $mylasso = $filter->args->[0] if (@{$filter->args});
+#    # XXX The following, along with most of the surrounding code, is actually dead now. However, realigning to be more "true" and match surrounding.
+#    # gather lasso, as with $ou
+    my $lasso = $args{lasso};
+#    if (my ($filter) = $query->parse_tree->find_filter('lasso')) {
+#            $lasso = $filter->args->[0] if (@{$filter->args});
 #    }
-#    $mylasso = actor::org_unit->search( { name => $mylasso } )->next->id if ($mylasso and $mylasso !~ /^\d+$/);
-
-
-    # if we have a lasso, go with that, otherwise ... ou
-    $ou = $lasso if ($lasso);
+#    # search by name if an id (number) wasn't given
+#    $lasso = actor::org_lasso->search( { name => $lasso } )->next->id if ($lasso and $lasso !~ /^\d+$/);
+#
+#    # gather lasso org list
+#    my $lasso_orgs = [];
+#    $lasso_orgs = [actor::org_lasso_map->search( { lasso => $lasso } )] if ($lasso);
+#
+#
+##    # XXX once we have org_unit containers, we can make user-defined lassos .. WHEEE
+##    # gather user lasso, as with $ou and lasso
+    my $mylasso = $args{my_lasso};
+##    if (my ($filter) = $query->parse_tree->find_filter('my_lasso')) {
+##            $mylasso = $filter->args->[0] if (@{$filter->args});
+##    }
+##    $mylasso = actor::org_unit->search( { name => $mylasso } )->next->id if ($mylasso and $mylasso !~ /^\d+$/);
+#
+#
+#    # if we have a lasso, go with that, otherwise ... ou
+#    $ou = $lasso if ($lasso);
 
     # gather the preferred OU, if one is specified, as with $ou
     my $pref_ou = $args{pref_ou};
-    $log->info("pref_ou = $pref_ou");
     if (my ($filter) = $query->parse_tree->find_filter('pref_ou')) {
             $pref_ou = $filter->args->[0] if (@{$filter->args});
     }
@@ -3303,6 +3308,7 @@ sub query_parser_fts_wrapper {
 
     # XXX All of the following, down to the 'return' is basically dead code. someone higher up should handle it
     $query = "site($args{org_unit}) $query" if ($args{org_unit});
+    $query = "lasso($args{lasso}) $query" if ($args{lasso});
     $query = "depth($args{depth}) $query" if (defined($args{depth}));
     $query = "sort($args{sort}) $query" if ($args{sort});
     $query = "core_limit($args{core_limit}) $query" if ($args{core_limit});
index ef231fe..1c6cb78 100644 (file)
@@ -407,11 +407,13 @@ sub load_common {
 
     $self->extract_copy_location_group_info;
     $ctx->{search_ou} = $self->_get_search_lib();
+    $ctx->{search_scope} = $self->cgi->param('search_scope');
     $self->staff_saved_searches_set_expansion_state if $ctx->{is_staff};
     $self->load_eg_cache_hash;
     $self->load_copy_location_groups;
     $self->load_my_hold_subscriptions;
     $self->load_hold_subscriptions if $ctx->{is_staff};
+    $self->load_lassos;
     $self->staff_saved_searches_set_expansion_state if $ctx->{is_staff};
     $self->load_search_filter_groups($ctx->{search_ou});
     $self->load_org_util_funcs;
index d0860bc..748b79a 100644 (file)
@@ -138,6 +138,10 @@ sub _prepare_biblio_search {
     # is still empty up to this point, there is no query.  abandon ship.
     return () unless $query;
 
+    # The search_scope param comes from a mixed-use dropdown containing depth,
+    # lasso, and location_groups filters.
+    $query .= ' ' . $cgi->param('search_scope') if defined $cgi->param('search_scope');
+
     # sort is treated specially, even though it's actually a filter
     if (defined($cgi->param('sort'))) {
         $query =~ s/sort\([^\)]*\)//g;  # override existing sort(). no stacking.
index f741bca..4816c7e 100644 (file)
@@ -70,87 +70,9 @@ sub init_ro_object_cache {
     my $locale_subs = {};
     my $locale = $ctx->{locale};
 
-    # make all "field_safe" classes accesible by default in the template context
-    my @classes = grep {
-        ($Fieldmapper::fieldmap->{$_}->{field_safe} || '') =~ /true/i
-    } keys %{ $Fieldmapper::fieldmap };
-
-    for my $class (@classes) {
-
-        my $hint = $Fieldmapper::fieldmap->{$class}->{hint};
-        next if $hint eq 'aou'; # handled separately
-
-        my $ident_field =  $Fieldmapper::fieldmap->{$class}->{identity};
-        (my $eclass = $class) =~ s/Fieldmapper:://o;
-        $eclass =~ s/::/_/g;
-
-        my $list_key = "${hint}_list";
-        my $get_key = "get_$hint";
-        my $search_key = "search_$hint";
-
-        my $memcache_key = join('.', 'EGWeb',$locale,$hint) . '.';
-
-        # Retrieve the full set of objects with class $hint
-        $locale_subs->{$list_key} = sub {
-            my $from_memcache = 0;
-            my $list = $memcache->get_cache($memcache_key.'list');
-            if ($list) {
-                $cache{list}{$locale}{$hint} = $list;
-                $from_memcache = 1;
-            }
-            my $method = "retrieve_all_$eclass";
-            my $e = new_editor();
-            $cache{list}{$locale}{$hint} = $e->$method() unless $cache{list}{$locale}{$hint};
-            undef $e;
-            $memcache->put_cache($memcache_key.'list',$cache{list}{$locale}{$hint}) unless $from_memcache;
-            return $cache{list}{$locale}{$hint};
-        };
-
-        # locate object of class $hint with Ident field $id
-        $cache{map}{$hint} = {};
-        $locale_subs->{$get_key} = sub {
-            my $id = shift;
-            return $cache{map}{$locale}{$hint}{$id} if $cache{map}{$locale}{$hint}{$id};
-            ($cache{map}{$locale}{$hint}{$id}) = grep { $_->$ident_field eq $id } @{$locale_subs->{$list_key}->()};
-            return $cache{map}{$locale}{$hint}{$id};
-        };
-
-        # search for objects of class $hint where field=value
-        $cache{search}{$hint} = {};
-        $locale_subs->{$search_key} = sub {
-            my ($field, $val, $filterfield, $filterval) = @_;
-            my $method = "search_$eclass";
-            my $cacheval = $val;
-            my $scalar_cacheval = 1;
-
-            if (ref $val) {
-                $scalar_cacheval = 0;
-                $val = [sort(@$val)] if ref $val eq 'ARRAY';
-                $cacheval = OpenSRF::Utils::JSON->perl2JSON($val);
-                #$self->apache->log->info("cacheval : $cacheval");
-            }
-
-            my $search_obj = {$field => $val};
-            if($filterfield) {
-                $search_obj->{$filterfield} = $filterval;
-                $cacheval .= ':' . $filterfield . ':' . $filterval;
-            } elsif (
-                $scalar_cacheval
-                and $cache{list}{$locale}{$hint}
-                and !$cache{search}{$locale}{$hint}{$field}{$cacheval}
-            ) {
-                return $cache{search}{$locale}{$hint}{$field}{$cacheval} =
-                    [ grep { $_->$field() eq $val } @{$cache{list}{$locale}{$hint}} ];
-            }
-
-            my $e = new_editor();
-            $cache{search}{$locale}{$hint}{$field}{$cacheval} = $e->$method($search_obj)
-                unless $cache{search}{$locale}{$hint}{$field}{$cacheval};
-            undef $e;
-            return $cache{search}{$locale}{$hint}{$field}{$cacheval};
-        };
-    }
+    # Create special-purpose subs
 
+    # aou is special because it's tree-ish
     $locale_subs->{aou_tree} = sub {
 
         # fetch the org unit tree
@@ -190,6 +112,11 @@ sub init_ro_object_cache {
         return $cache{map}{$locale}{aou}{$org_id};
     };
 
+    # Returns a flat list of aout objects, sorted by depth and opac_label.
+    $locale_subs->{sorted_aout_list} = sub {
+        return [ sort { $a->depth() <=> $b->depth() || $a->opac_label() cmp $b->opac_label() } @{$locale_subs->{aout_list}->()} ];
+    };
+
     # Returns a flat list of aou objects.  often easier to manage than a tree.
     $locale_subs->{aou_list} = sub {
         $locale_subs->{aou_tree}->(); # force the org tree to load
@@ -326,6 +253,87 @@ sub init_ro_object_cache {
         return $cache{authority_fields}{$locale}{$control_set};
     };
 
+    # make all "field_safe" classes accesible by default in the template context
+    my @classes = grep {
+        ($Fieldmapper::fieldmap->{$_}->{field_safe} || '') =~ /true/i
+    } keys %{ $Fieldmapper::fieldmap };
+
+    for my $class (@classes) {
+
+        my $hint = $Fieldmapper::fieldmap->{$class}->{hint};
+        next if $hint eq 'aou'; # handled separately
+
+        my $ident_field =  $Fieldmapper::fieldmap->{$class}->{identity};
+        (my $eclass = $class) =~ s/Fieldmapper:://o;
+        $eclass =~ s/::/_/g;
+
+        my $list_key = "${hint}_list";
+        my $get_key = "get_$hint";
+        my $search_key = "search_$hint";
+
+        my $memcache_key = join('.', 'EGWeb',$locale,$hint) . '.';
+
+        # Retrieve the full set of objects with class $hint
+        $locale_subs->{$list_key} ||= sub {
+            my $from_memcache = 0;
+            my $list = $memcache->get_cache($memcache_key.'list');
+            if ($list) {
+                $cache{list}{$locale}{$hint} = $list;
+                $from_memcache = 1;
+            }
+            my $method = "retrieve_all_$eclass";
+            my $e = new_editor();
+            $cache{list}{$locale}{$hint} = $e->$method() unless $cache{list}{$locale}{$hint};
+            undef $e;
+            $memcache->put_cache($memcache_key.'list',$cache{list}{$locale}{$hint}) unless $from_memcache;
+            return $cache{list}{$locale}{$hint};
+        };
+
+        # locate object of class $hint with Ident field $id
+        $cache{map}{$hint} = {};
+        $locale_subs->{$get_key} ||= sub {
+            my $id = shift;
+            return $cache{map}{$locale}{$hint}{$id} if $cache{map}{$locale}{$hint}{$id};
+            ($cache{map}{$locale}{$hint}{$id}) = grep { $_->$ident_field eq $id } @{$locale_subs->{$list_key}->()};
+            return $cache{map}{$locale}{$hint}{$id};
+        };
+
+        # search for objects of class $hint where field=value
+        $cache{search}{$hint} = {};
+        $locale_subs->{$search_key} ||= sub {
+            my ($field, $val, $filterfield, $filterval) = @_;
+            my $method = "search_$eclass";
+            my $cacheval = $val;
+            my $scalar_cacheval = 1;
+
+            if (ref $val) {
+                $scalar_cacheval = 0;
+                $val = [sort(@$val)] if ref $val eq 'ARRAY';
+                $cacheval = OpenSRF::Utils::JSON->perl2JSON($val);
+                #$self->apache->log->info("cacheval : $cacheval");
+            }
+
+            my $search_obj = {$field => $val};
+            if($filterfield) {
+                $search_obj->{$filterfield} = $filterval;
+                $cacheval .= ':' . $filterfield . ':' . $filterval;
+            } elsif (
+                $scalar_cacheval
+                and $cache{list}{$locale}{$hint}
+                and !$cache{search}{$locale}{$hint}{$field}{$cacheval}
+            ) {
+                return $cache{search}{$locale}{$hint}{$field}{$cacheval} =
+                    [ grep { $_->$field() eq $val } @{$cache{list}{$locale}{$hint}} ];
+            }
+
+            my $e = new_editor();
+            $cache{search}{$locale}{$hint}{$field}{$cacheval} = $e->$method($search_obj)
+                unless $cache{search}{$locale}{$hint}{$field}{$cacheval};
+            undef $e;
+            return $cache{search}{$locale}{$hint}{$field}{$cacheval};
+        };
+    }
+
     $ctx->{$_} = $locale_subs->{$_} for keys %$locale_subs;
     $ro_object_subs->{$locale} = $locale_subs;
 }
@@ -694,7 +702,7 @@ sub load_copy_location_groups {
                 }
             }
         },
-        {order_by => {acplg => 'pos'}}
+        {order_by => {acplg => ['pos','name']}}
     ]);
 
     my %buckets;
@@ -736,6 +744,24 @@ sub load_my_hold_subscriptions {
         ) : [];
 }
 
+sub load_lassos {
+    my $self = shift;
+    my $ctx = $self->ctx;
+
+    # User can access global lassos and those at the current search lib
+    my $direct_lassos = $self->editor->search_actor_org_lasso_map(
+        { org_unit => $ctx->{search_ou} }
+    );
+    $direct_lassos = [ map { $_->lasso } @$direct_lassos];
+
+    my $lassos = $self->editor->search_actor_org_lasso(
+        { '-or' => { global => 't', @$direct_lassos ? (id => { in => $direct_lassos}) : () } }
+    );
+
+    $ctx->{lassos} = [ sort { $a->name cmp $b->name } @$lassos ];
+    $self->apache->log->info("Fetched ".scalar(@$lassos)." lassos");
+}
+
 sub set_file_download_headers {
     my $self = shift;
     my $filename = shift;
index 33ae628..6fb6a49 100644 (file)
@@ -399,7 +399,8 @@ CREATE TRIGGER actor_org_unit_parent_protect_trigger
 
 CREATE TABLE actor.org_lasso (
     id      SERIAL  PRIMARY KEY,
-    name       TEXT    UNIQUE
+    name       TEXT    UNIQUE,
+    global  BOOL    NOT NULL DEFAULT FALSE
 );
 
 CREATE TABLE actor.org_lasso_map (
index dd3f884..363a8b4 100644 (file)
@@ -1952,7 +1952,9 @@ INSERT INTO permission.perm_list ( id, code, description ) VALUES
  ( 627, 'SSO_ADMIN', oils_i18n_gettext(627,
     'Modify patron SSO settings', 'ppl', 'description')),
  ( 628, 'MANAGE_HOLD_GROUPS', oils_i18n_gettext(628,
-    'Manage batch (subscription) hold events', 'ppl', 'description'))
+    'Manage batch (subscription) hold events', 'ppl', 'description')),
+ ( 629, 'ADMIN_LIBRARY_GROUPS', oils_i18n_gettext(629,
+    'Administer library groups', 'ppl', 'description'))
 ;
 
 
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.lassos.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.lassos.sql
new file mode 100644 (file)
index 0000000..f4c333a
--- /dev/null
@@ -0,0 +1,29 @@
+BEGIN;
+
+SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+
+-- XXX Committer: confirm ID below is the next available!
+INSERT INTO permission.perm_list (id, code, description)
+    VALUES ( 629, 'ADMIN_LIBRARY_GROUPS', 'Administer library groups');
+
+INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
+VALUES (
+    'eg.grid.admin.server.actor.org_lasso', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.admin.server.actor.org_lasso',
+        'Grid Config: admin.server.actor.org_lasso',
+        'cwst', 'label'
+    )
+), (
+    'eg.grid.admin.server.actor.org_lasso_map', 'gui', 'object',
+    oils_i18n_gettext(
+        'eg.grid.admin.server.actor.org_lasso_map',
+        'Grid Config: admin.server.actor.org_lasso_map',
+        'cwst', 'label'
+    )
+);
+
+ALTER TABLE actor.org_lasso ADD COLUMN global BOOL NOT NULL DEFAULT FALSE;
+
+COMMIT;
+
index 7ac166d..0736738 100644 (file)
 [%
         IF adv_chunk.adv_special;
             SWITCH adv_chunk.adv_special;
+                CASE "scope_selector";
+                    PROCESS "opac/parts/org_selector.tt2";
+                        select_scope_label = l("Select search scope");
+                        INCLUDE build_scope_selector arialabel=select_scope_label value=ctx.search_scope
+                          id=adv_chunk.id name='search_scope' show_loc_groups=1;
+
                 CASE "lib_selector";
                     PROCESS "opac/parts/org_selector.tt2";
-                        INCLUDE build_org_selector show_loc_groups=1 id=adv_chunk.id %]
+                        INCLUDE build_org_selector show_loc_groups=0 id=adv_chunk.id %]
                             <div class="adv_search_available">
                                 <input type='checkbox' name="modifier"
                                     value="available"[% CGI.param('modifier').grep('available').size ? ' checked="checked"' : '' %]
@@ -76,6 +82,7 @@
                                 <label for='opac.result.limit2avail'>
                                     [% l("Limit to Available") %]</label>
                             </div>
+
 [%
                 CASE "pub_year"; %]
                             <select name='pubdate'
index 48b4f0f..a1438b7 100644 (file)
@@ -148,6 +148,7 @@ search.adv_config = [
     {adv_label => l("Literary Form"), adv_attr => "lit_form", id => 'adv_selector_lit_form'},
     {adv_label => l("Shelving Location"), adv_special => "copy_location", id => 'adv_copy_location_selector', js_only => 1, adv_break => 1},
     {adv_label => l("Search Library"), adv_special => "lib_selector", id => 'adv_org_selector'},
+    {adv_label => l("Where"), adv_special => "scope_selector", id => 'adv_scope_selector'},
     {adv_label => l("Publication Year"), adv_special => "pub_year", id => 'adv_selector_pub_year'},
     {adv_label => l("Sort Results"), adv_special => "sort_selector", id => 'adv_selector_sort_results'},
 ];
index ab4d7c9..e2890e8 100644 (file)
@@ -131,4 +131,69 @@ BLOCK build_org_selector;
 
         [%- END %]
     </select>
+[%- END;
+
+BLOCK build_scope_selector;
+    IF !name; 
+        name = id;
+    END;
+    IF !value;
+        value = loc_value;
+    END;
+
+    ou_id = ctx.search_ou;
+    context_org = ctx.get_aou(ou_id);
+
+    # if the selected org unit is out of hiding scope, 
+    # disable the ou-hide scoping altogether.
+    hiding_disabled = ctx.org_hiding_disabled(value);
+    hiding_depth = ctx.get_org_setting(ou_id, 'opac.org_unit_hiding.depth') -%]
+
+    <select [% IF id %] id='[% id %]' [% END -%]
+            title='[% arialabel || l("Select Library") %]' 
+            name='[% name %]'>
+            <option value=''>[% l('No Restrictions') | html %]</option>
+
+        <optgroup label="[% l('Search Scope') %]"> [%
+        FOR d IN ctx.sorted_aout_list(); # sorts by depth and opac_label
+            NEXT IF !hiding_disabled AND hiding_depth > 0 AND d.depth < hiding_depth;
+            NEXT IF d.depth > context_org.ou_type.depth; # only show ancestors-and-self
+            opt_value = 'depth(' _ d.depth _ ')';
+            selected = '';
+            IF value == opt_value; selected = 'selected="selected"'; END %]
+            <option value='[% opt_value %]' [% selected %]> 
+                [% d.opac_label | html %]
+            </option>
+        [% END %]
+        </optgroup>
+
+        [% IF ctx.lassos.size > 0;
+            %] <optgroup label="[% l('Library Groups') %]"> [%
+            FOR lasso IN ctx.lassos;
+                opt_value = 'lasso(' _ lasso.id _ ')';
+                selected = '';
+                IF value == opt_value; selected = 'selected="selected"'; END %]
+                <option value='[% opt_value %]' [% selected %]> 
+                    [% lasso.name | html %]
+                </option>
+            [% END %]
+            </optgroup>
+        [% END;
+
+        IF show_loc_groups AND ctx.copy_location_groups.size > 0;
+            %] <optgroup label="[% l('Location Groups') %]"> [%
+            FOR lc_ou_id IN ctx.copy_location_groups.keys;
+                FOR grp IN ctx.copy_location_groups.$lc_ou_id;
+                    opt_value = 'location_groups(' _ grp.id _ ')';
+                    selected = '';
+                    IF value == opt_value; selected = 'selected="selected"'; END %]
+                    <option value='[% opt_value %]' [% selected %]> 
+                        [% grp.name | html %]
+                    </option>
+                [% END %]
+            [% END %]
+            </optgroup>
+        [% END %]
+
+    </select>
 [%- END %]
index f9fb92b..2e5f6e3 100644 (file)
@@ -3,7 +3,7 @@
 
 # We need to ignore some filters in our count
 
-fignore = ['location_groups','site','core_limit','limit','badge_orgs','badges','estimation_strategy','depth'];
+fignore = ['lasso','location_groups','site','core_limit','limit','badge_orgs','badges','estimation_strategy','depth'];
 fcount = 0;
 FOR f IN ctx.query_struct.filters;
     IF fignore.grep('^' _ f.name _ '$').size;
@@ -97,7 +97,15 @@ END;
             l('Library: ');
             select_lib_label = l("Select search library");
             INCLUDE build_org_selector arialabel=select_lib_label 
-              id='search_org_selector' show_loc_groups=1
+              id='search_org_selector' show_loc_groups=0
+        -%]
+        </label>
+        <label id="search_scope_label" for="search_scope_selector">
+        [%- 
+            l('Where: ');
+            select_scope_label = l("Select search scope");
+            INCLUDE build_scope_selector arialabel=select_scope_label value=ctx.search_scope
+              id='search_scope_selector' name='search_scope' show_loc_groups=1
         -%]
         </label>
       <span>
index 1377308..07a36da 100644 (file)
@@ -34,6 +34,8 @@
     ,[ l('Import Match Sets'), "./admin/server/vandelay/match_set" ]
     ,[ l('Item Statuses'), "./admin/server/legacy/config/copy_status" ]
     ,[ l('Item Tag Types'), "./admin/server/config/copy_tag_type" ]
+    ,[ l('Library Groups'), "/eg2/staff/admin/server/actor/org_lasso" ]
+    ,[ l('Library Group Maps'), "/eg2/staff/admin/server/actor/org_lasso_map" ]
     ,[ l('MARC Coded Value Maps'), "./admin/server/config/coded_value_map" ]
     ,[ l('MARC Import Remove Fields'), "./admin/server/vandelay/import_bib_trash_group" ]
     ,[ l('MARC Record Attributes'), "./admin/server/config/record_attr_definition" ]