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

Evergreen Git git at git.evergreen-ils.org
Thu Feb 9 16:03:21 EST 2017


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  c9afb4556567a1997f38143c70341611894c0ec3 (commit)
       via  38ca8cc181bb79151803bc6605f843d9d99d7762 (commit)
       via  32d9fd055648778410f9b5e72ec98ae522affaf9 (commit)
       via  f12cc285f3e68ebcb2f95d942556d3bd87e392dc (commit)
       via  9a03ed40736c38b157b2cc42187063e68a41728c (commit)
       via  6e32c5bf4babe4d445446df9cafcffc75d298123 (commit)
       via  b8d8e210dfa6c2a247f14f2d468960a795326867 (commit)
       via  fc8af9f703333ba9b963854f95e3755b9c838f08 (commit)
       via  6ebf34a1e18f1bd6215567a9b51b94d0bd24c35b (commit)
       via  3a40d740494eca897efb17c0a0ac48428855b7c1 (commit)
       via  3d917fb818193f409d03845eb1b63b3d6956399f (commit)
       via  a341d347ded57a7bb0a4e11f980486e9809bbfd7 (commit)
       via  3755e827644ea52da5bfed0289ed900ee9ecf1ab (commit)
       via  c6cb2c8a7f15f965c89ea676adbf6f8423824df8 (commit)
       via  fe51c342c8aeab90b98df5bf33a5e4a7400cac54 (commit)
       via  743cb27c2a005adbde5488e5c877d3ad7e96872c (commit)
       via  2b808b2e7b493a9483f238551ae3c38f5d7703ad (commit)
      from  47a6251477d9d872c7e97f239c2286917fb29fb1 (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 c9afb4556567a1997f38143c70341611894c0ec3
Author: Kathy Lussier <klussier at masslnc.org>
Date:   Thu Feb 9 15:58:17 2017 -0500

    LP#1005040: Stamping upgrade script for realign search layers
    
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/sql/Pg/002.schema.config.sql b/Open-ILS/src/sql/Pg/002.schema.config.sql
index 6c61454..a0c51b3 100644
--- a/Open-ILS/src/sql/Pg/002.schema.config.sql
+++ b/Open-ILS/src/sql/Pg/002.schema.config.sql
@@ -91,7 +91,7 @@ CREATE TRIGGER no_overlapping_deps
     BEFORE INSERT OR UPDATE ON config.db_patch_dependencies
     FOR EACH ROW EXECUTE PROCEDURE evergreen.array_overlap_check ('deprecates');
 
-INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1006', :eg_version); -- DPearl/kmlussier
+INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1007', :eg_version); -- gmcharlt/kmlussier
 
 CREATE TABLE config.bib_source (
 	id		SERIAL	PRIMARY KEY,
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.give-crad-human-descriptions.sql b/Open-ILS/src/sql/Pg/upgrade/1007.data.give-crad-human-descriptions.sql
similarity index 94%
rename from Open-ILS/src/sql/Pg/upgrade/XXXX.data.give-crad-human-descriptions.sql
rename to Open-ILS/src/sql/Pg/upgrade/1007.data.give-crad-human-descriptions.sql
index ee52abd..5eb5588 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.give-crad-human-descriptions.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/1007.data.give-crad-human-descriptions.sql
@@ -1,3 +1,5 @@
+SELECT evergreen.upgrade_deps_block_check('1007', :eg_version);
+
 BEGIN;
 
 UPDATE config.record_attr_definition

commit 38ca8cc181bb79151803bc6605f843d9d99d7762
Author: Mike Rylander <mrylander at gmail.com>
Date:   Thu Feb 9 14:45:55 2017 -0500

    Adjust comment about apostrophes in opensearch code.  This is a marker for future work.
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/SuperCat.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/SuperCat.pm
index a599706..e9e3487 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/SuperCat.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/SuperCat.pm
@@ -1437,9 +1437,10 @@ sub opensearch_feed {
 
     my $org_unit = get_ou($org);
 
-    # Apostrophes break search and get indexed as spaces anyway
-    # XXX ^that's kinda a lie ...
     my $safe_terms = $terms;
+
+    # XXX Apostrophes used to break search, but no longer do.  The following
+    # XXX line breaks phrase searching in OpenSearch, and should be removed.
     $safe_terms =~ s{'}{ }go;
     
     my $query_terms = 'site('.$org_unit->[0]->shortname.") $safe_terms";

commit 32d9fd055648778410f9b5e72ec98ae522affaf9
Author: Kathy Lussier <klussier at masslnc.org>
Date:   Thu Feb 9 13:41:06 2017 -0500

    LP#1005040: Release notes entry for advanced search limiter improvements
    
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/docs/RELEASE_NOTES_NEXT/OPAC/advanced_search_limiters.adoc b/docs/RELEASE_NOTES_NEXT/OPAC/advanced_search_limiters.adoc
new file mode 100644
index 0000000..99a4e53
--- /dev/null
+++ b/docs/RELEASE_NOTES_NEXT/OPAC/advanced_search_limiters.adoc
@@ -0,0 +1,12 @@
+Advanced Search Limiters Enhancement
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Advanced search limiters will no longer propogate to the basic search box in
+the catalog. Instead, the limiters applied to the search will appear in the
+left sidebar of the search results screen where they can be easily cleared by
+clicking an 'x.' On a small, mobile device, the advanced search limiters can
+be seen by clicking an 'x filter applied' link or by clicking the 'Refine
+these results' button that typically shows facets to the user.
+
+The selected limiters will be applied to any search from the search bar until:
+ * The user actively removes the filters from the search or
+ * The user starts a new basic or advanced search from scratch.

commit f12cc285f3e68ebcb2f95d942556d3bd87e392dc
Author: Mike Rylander <mrylander at gmail.com>
Date:   Tue Dec 20 15:29:50 2016 -0500

    LP#1005040: Styling cleanup for filter display
    
    1) Use a unicode X instead of the string "Remove"
    2) Provide a border around filters
    3) Label filters with "Filtered by"
    4) Only show "[X filters applied ]" when in mobile mode, and make it
       work like the "Refine these results" button.
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/src/templates/opac/css/style.css.tt2 b/Open-ILS/src/templates/opac/css/style.css.tt2
index 77b3b62..413c3c9 100644
--- a/Open-ILS/src/templates/opac/css/style.css.tt2
+++ b/Open-ILS/src/templates/opac/css/style.css.tt2
@@ -1073,6 +1073,14 @@ div#facet_sidebar {
     background: [% css_colors.background_invert %] !important;
 }
 
+.filter_box_label {
+    color: [% css_colors.background_invert %];
+    font-weight:bold;
+    padding-top:4px;
+    padding-bottom:4px;
+    padding-left:12px;
+}
+
 .facet_box_temp .header .title {
     float:left;
     padding-top:6px;
@@ -1102,6 +1110,16 @@ div#facet_sidebar {
     overflow: hidden;
 }
 
+.filter_box_wrapper {
+    margin-bottom: 3px;
+    padding: 2px;
+    border: 1px solid [% css_colors.background_invert %];
+    -moz-border-radius: 3px;
+    border-radius: 3px;
+    font-weight:bold;
+    padding-top:4px;
+}
+
 .facet_template {
     box-sizing: border-box;
     -moz-box-sizing: border-box;
@@ -1731,7 +1749,7 @@ a.preflib_change {
     clear: both;
 }
 
-.small_view_only, #refine_hits, #return_to_hits {
+.small_view_only, #filter_hits, #refine_hits, #return_to_hits {
     display: none;
 }
 
@@ -1854,7 +1872,7 @@ a.preflib_change {
     #dash_wrapper .opac-button {
         top: 0px;
     }
-    .small_view_only {
+    .small_view_only, #filter_hits {
         display: inline !important;
     }
     #dash_identity a {
diff --git a/Open-ILS/src/templates/opac/parts/result/adv_filter.tt2 b/Open-ILS/src/templates/opac/parts/result/adv_filter.tt2
index dbe712f..07efb48 100644
--- a/Open-ILS/src/templates/opac/parts/result/adv_filter.tt2
+++ b/Open-ILS/src/templates/opac/parts/result/adv_filter.tt2
@@ -1,32 +1,21 @@
-<div class="facet_box_wrapper filter_box_wrapper">
-[%
-
-# don't display a box for the search_format filter,
-# as that's got its own widget
-ignore_filters = ['search_format'];
+[%-
 
 pubdate_filters = ['date1', 'before', 'after', 'between'];
 
 FOR filter IN ctx.query_struct.filters;
     fname = filter.name;
-    IF ignore_filters.grep('^' _ fname _ '$').size;
-      NEXT;
-    END;
-
     fvalues = filter.args;
     crad = ctx.get_crad(fname);
 
     # will be some special ones, like locations
     IF crad AND NOT pubdate_filters.grep('^' _ filter.name _ '$').size;
         remove_filter = 'fi:' _ fname;
-%]
+-%]
     <div class="facet_box_temp filter_box_temp">
         <div class="header">
             <a class="button"
               title="[% l('Remove [_1] filter', (crad.description || crad.label)) %]"
-              href="[% mkurl('', {}, [remove_filter]) %]" rel="nofollow" vocab="">
-              [% l("Remove") %]
-            </a>
+              href="[% mkurl('', {}, [remove_filter]) %]" rel="nofollow" vocab=""> ✘ </a>
             <h4 class="title">[% IF filter.negate; l('Not'); END %] [% (crad.description || crad.label) | html %]</h4>
         </div>
         <div class="box_wrapper">
@@ -50,16 +39,14 @@ FOR filter IN ctx.query_struct.filters;
             </div>
         </div> <!-- box_wrapper -->
     </div> <!-- facet_box_temp -->
-    [% END; # IF crad %]
+    [%- END; # IF crad -%]
 
-[%  IF filter.name == 'locations'; locs = ctx.search_acpl('id',filter.args) %]
+[%-  IF filter.name == 'locations'; locs = ctx.search_acpl('id',filter.args) -%]
     <div class="facet_box_temp filter_box_temp">
         <div class="header">
             <a class="button"
               title="[% l('Remove location filter') %]"
-              href="[% mkurl('', {}, ['fi:locations']) %]" rel="nofollow" vocab="">
-              [% l("Remove") %]
-            </a>
+              href="[% mkurl('', {}, ['fi:locations']) %]" rel="nofollow" vocab=""> ✘ </a>
             <h4 class="title">[% IF filter.negate; l('Not'); END %] [% l('Locations') %]</h4>
         </div>
         <div class="box_wrapper">
@@ -78,19 +65,17 @@ FOR filter IN ctx.query_struct.filters;
             </div>
         </div> <!-- box_wrapper -->
     </div> <!-- facet_box_temp -->
-[% END; # IF locations %]
+[%- END; # IF locations -%]
 
-[% IF pubdate_filters.grep('^' _ filter.name _ '$').size;
+[%- IF pubdate_filters.grep('^' _ filter.name _ '$').size;
     date1 = CGI.param('date1');
     date2 = CGI.param('date2');
-%]
+-%]
     <div class="facet_box_temp filter_box_temp">
         <div class="header">
             <a class="button"
               title="[% l('Remove publication date filter') %]"
-              href="[% mkurl('', {}, ['pubdate', 'date1', 'date2']) %]" rel="nofollow" vocab="">
-              [% l("Remove") %]
-            </a>
+              href="[% mkurl('', {}, ['pubdate', 'date1', 'date2']) %]" rel="nofollow" vocab=""> ✘ </a>
             <h4 class="title">[% IF filter.negate; l('Not'); END %] [% l('Publication Year') %]</h4>
         </div>
         <div class="box_wrapper">
@@ -103,8 +88,5 @@ FOR filter IN ctx.query_struct.filters;
             </div>
         </div> <!-- box_wrapper -->
     </div> <!-- facet_box_temp -->
-[% END; # IF pubdate_filters %]
-
-[% END; # FOR %]
-</div> <!-- facet_box_wrapper -->
-
+[%- END; # IF pubdate_filters -%]
+[%- END; # FOR -%]
diff --git a/Open-ILS/src/templates/opac/parts/result/table.tt2 b/Open-ILS/src/templates/opac/parts/result/table.tt2
index 7e47daf..f4ff94a 100644
--- a/Open-ILS/src/templates/opac/parts/result/table.tt2
+++ b/Open-ILS/src/templates/opac/parts/result/table.tt2
@@ -8,6 +8,21 @@
 
     result_count = ctx.result_start;
 
+    # don't display a box for the search_format filter,
+    # as that's got its own widget
+    ignore_filters = ['search_format'];
+
+    trimmed_filters = [];
+    FOR filter IN ctx.query_struct.filters;
+        fname = filter.name;
+        IF ignore_filters.grep('^' _ fname _ '$').size;
+            NEXT;
+        END;
+        trimmed_filters.push(filter);
+    END;
+
+    ctx.query_struct.filters = trimmed_filters;
+
 %]
 
 [% PROCESS "opac/parts/result/paginate.tt2" %] 
@@ -25,8 +40,16 @@
     <h3 class="sr-only">[% l('Saved Searches') %]</h3>
     [% INCLUDE "opac/parts/staff_saved_searches.tt2" %]
     [%-  END %]
-    <h3 class="sr-only">[% l('Search Results filters') %]</h3>
-    [% INCLUDE 'opac/parts/result/adv_filter.tt2' %]
+    [% IF ctx.query_struct.filters.size > 0 %]
+        [% stuff = INCLUDE 'opac/parts/result/adv_filter.tt2' %]
+        [% IF stuff %]
+        <h3 class="sr-only">[% l('Search Results filters') %]</h3>
+        <div class="facet_box_wrapper filter_box_wrapper">
+            <div class="filter_box_label">[% l('Filtered By') %]</div>
+            [% stuff %]
+        </div>
+        [% END %]
+    [% END %]
     <h3 class="sr-only">[% l('Search Results facets') %]</h3>
     [% INCLUDE 'opac/parts/result/facets.tt2' %]
     <h3 class="sr-only">[% l('Search Results List') %]</h3>
diff --git a/Open-ILS/src/templates/opac/parts/searchbar.tt2 b/Open-ILS/src/templates/opac/parts/searchbar.tt2
index 97517c0..9d6a0dd 100644
--- a/Open-ILS/src/templates/opac/parts/searchbar.tt2
+++ b/Open-ILS/src/templates/opac/parts/searchbar.tt2
@@ -126,9 +126,9 @@ END;
         </form>
     [% END %]
     [% IF (is_advanced AND NOT is_special) AND CGI.param('qtype') %]
-    <div class="refine_search">
+    <div class="refine_search result_block_visible">
         [% IF fcount > 0 %]
-        [ [% l('[quant,_1,filter,filters] applied', fcount) %] ]
+        <span id="filter_hits">[ <a href="#" onclick="getFacety();">[% l('[quant,_1,filter,filters] applied', fcount) %]</a> ]</span>
         [% END %]
         [ <a href="[% mkurl(ctx.opac_root _ '/advanced') %]">[%
             l('Refine My Original Search')

commit 9a03ed40736c38b157b2cc42187063e68a41728c
Author: Mike Rylander <mrylander at gmail.com>
Date:   Mon Feb 17 13:00:05 2014 -0500

    LP1281280: Allow test script to run without a full installation
    
    --no-connect causes it to do what it says on the tin.
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/src/support-scripts/test-scripts/query_parser.pl b/Open-ILS/src/support-scripts/test-scripts/query_parser.pl
index 3c647da..6f835de 100755
--- a/Open-ILS/src/support-scripts/test-scripts/query_parser.pl
+++ b/Open-ILS/src/support-scripts/test-scripts/query_parser.pl
@@ -34,6 +34,7 @@ $query = 'concerto && (piano || item_type(a)) && (music || item_form(b))';
 my $superpage = 1;
 my $superpage_size = 1000;
 my $core_limit = 25000;
+my $noconnect;
 my $debug;
 my $config = '/openils/conf/opensrf_core.xml';
 my $quiet = 0;
@@ -43,13 +44,12 @@ GetOptions(
     'superpage-size=i' => \$superpage_size,
     'core-limit=i' => \$core_limit,
     'query=s' => \$query,
+    'no-connect' => \$noconnect,
     'debug' => \$debug,
     'quiet' => \$quiet,
     'config=s' => \$config
 );
 
-osrf_connect($config);
-
 my $parser = OpenILS::Application::Storage::Driver::Pg::QueryParser->new( 
     superpage_size => $superpage_size, 
     superpage => $superpage, 
@@ -58,42 +58,46 @@ my $parser = OpenILS::Application::Storage::Driver::Pg::QueryParser->new(
     debug => $debug 
 );
 
-# load the parser config
-my $cstore = OpenSRF::AppSession->create( 'open-ils.cstore' );
-$parser->initialize(
-    config_record_attr_index_norm_map =>
-        $cstore->request(
-            'open-ils.cstore.direct.config.record_attr_index_norm_map.search.atomic',
-            { id => { "!=" => undef } },
-            { flesh => 1, flesh_fields => { crainm => [qw/norm/] }, order_by => [{ class => "crainm", field => "pos" }] }
-        )->gather(1),
-    search_relevance_adjustment         =>
-        $cstore->request(
-            'open-ils.cstore.direct.search.relevance_adjustment.search.atomic',
-            { id => { "!=" => undef } }
-        )->gather(1),
-    config_metabib_field                =>
-        $cstore->request(
-            'open-ils.cstore.direct.config.metabib_field.search.atomic',
-            { id => { "!=" => undef } }
-        )->gather(1),
-    config_metabib_search_alias         =>
-        $cstore->request(
-            'open-ils.cstore.direct.config.metabib_search_alias.search.atomic',
-            { alias => { "!=" => undef } }
-        )->gather(1),
-    config_metabib_field_index_norm_map =>
-        $cstore->request(
-            'open-ils.cstore.direct.config.metabib_field_index_norm_map.search.atomic',
-            { id => { "!=" => undef } },
-            { flesh => 1, flesh_fields => { cmfinm => [qw/norm/] }, order_by => [{ class => "cmfinm", field => "pos" }] }
-        )->gather(1),
-    config_record_attr_definition       =>
-        $cstore->request(
-            'open-ils.cstore.direct.config.record_attr_definition.search.atomic',
-            { name => { "!=" => undef } }
-        )->gather(1),
-);
+if (!$noconnect) {
+    osrf_connect($config);
+
+    # load the parser config
+    my $cstore = OpenSRF::AppSession->create( 'open-ils.cstore' );
+    $parser->initialize(
+        config_record_attr_index_norm_map =>
+            $cstore->request(
+                'open-ils.cstore.direct.config.record_attr_index_norm_map.search.atomic',
+                { id => { "!=" => undef } },
+                { flesh => 1, flesh_fields => { crainm => [qw/norm/] }, order_by => [{ class => "crainm", field => "pos" }] }
+            )->gather(1),
+        search_relevance_adjustment         =>
+            $cstore->request(
+                'open-ils.cstore.direct.search.relevance_adjustment.search.atomic',
+                { id => { "!=" => undef } }
+            )->gather(1),
+        config_metabib_field                =>
+            $cstore->request(
+                'open-ils.cstore.direct.config.metabib_field.search.atomic',
+                { id => { "!=" => undef } }
+            )->gather(1),
+        config_metabib_search_alias         =>
+            $cstore->request(
+                'open-ils.cstore.direct.config.metabib_search_alias.search.atomic',
+                { alias => { "!=" => undef } }
+            )->gather(1),
+        config_metabib_field_index_norm_map =>
+            $cstore->request(
+                'open-ils.cstore.direct.config.metabib_field_index_norm_map.search.atomic',
+                { id => { "!=" => undef } },
+                { flesh => 1, flesh_fields => { cmfinm => [qw/norm/] }, order_by => [{ class => "cmfinm", field => "pos" }] }
+            )->gather(1),
+        config_record_attr_definition       =>
+            $cstore->request(
+                'open-ils.cstore.direct.config.record_attr_definition.search.atomic',
+                { name => { "!=" => undef } }
+            )->gather(1),
+    );
+}
 
 $parser->parse;
 

commit 6e32c5bf4babe4d445446df9cafcffc75d298123
Author: Mike Rylander <mrylander at gmail.com>
Date:   Tue Nov 15 14:26:48 2016 -0500

    LP1281280: Improve query tree compression
    
    In addition to collapsing adjacent nodes sharing the same boolean operator,
    we'll now also do the following two things: collapse filters, facets and
    modifiers when there exists only a single subnode; and absorb single node
    subplans.
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/QueryParser.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/QueryParser.pm
index 7d3b86d..090b944 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/QueryParser.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/QueryParser.pm
@@ -1552,6 +1552,32 @@ sub pullup {
     warn "Entering pullup depth ". $self->plan_level . "\n"
         if $self->QueryParser->debug;
 
+    my $old_qnodes = $self->query_nodes; # we will ignore all but ::node objects in this list
+    warn @$old_qnodes . " plans at pullup depth ". $self->plan_level . "\n"
+        if $self->QueryParser->debug;
+
+    # PASS 0: If I only have one child, collapse filters/modifiers into me 
+    if (@$old_qnodes == 1) {
+        my $kid = $$old_qnodes[0];
+        if ($kid->isa('QueryParser::query_plan')) {
+            $self->add_filter($_) foreach @{$kid->filters};
+            $self->add_facet($_) foreach @{$kid->facets};
+            $self->add_modifier($_) foreach @{$kid->modifiers};
+            $kid->{filters} = [];
+            $kid->{facets} = [];
+            $kid->{modifiers} = [];
+
+            my $kid_qnodes = $kid->query_nodes;
+            if (@$kid_qnodes == 1) { # And if my kid is a plan with only one node, absorb that
+                my $a = $$kid_qnodes[0];
+                if ($a->isa('QueryParser::query_plan')) {
+                    $self->{query} = [$a];
+                    return $self;
+                }
+            }
+        }
+    }
+
     # PASS 1: loop, attempting to pull up simple nodes
     my @new_nodes;
     my $prev_node;
@@ -1559,10 +1585,6 @@ sub pullup {
 
     my $prev_joiner;
 
-    my $old_qnodes = $self->query_nodes; # we will ignore all but ::node objects in this list
-    warn @$old_qnodes . " plans at pullup depth ". $self->plan_level . "\n"
-        if $self->QueryParser->debug;
-
     while (my $p = shift(@$old_qnodes)) {
 
         # joiners and ::node's get pushed onto the stack of new nodes
@@ -1618,7 +1640,6 @@ sub pullup {
     warn @new_nodes . " nodes after pullup of simple nodes at depth ". $self->plan_level . "\n"
         if $self->QueryParser->debug;
 
-
     # PASS 2: merge adjacent ::node's
     my $dangling = 0;
     my $sync_node = $prev_joiner = undef;

commit b8d8e210dfa6c2a247f14f2d468960a795326867
Author: Mike Rylander <mrylander at gmail.com>
Date:   Mon Feb 17 13:00:12 2014 -0500

    LP1281280: Implement adjacent-node pull-up optimization
    
    If one has a long list of boolean operations, such as can
    be generated by third-party products looking for a book that
    might have one of several different ISBNs, those are currently
    searched separatelly, in a deep nested set of joined full-text
    queries.  This behavior was introduced to address problems of
    non-deterministic grouping of boolean operations, along with
    them generally not working in complex situations.  We call
    the mechanism "boolean push-down", because it explicitly
    bifurcates the tree at each boolean operator.
    
    This is suboptimal in the case of adjacent, like boolean ops.
    
    This commit re-compresses the parse tree for adjacent nodes
    that use the same boolean operator and are composed of atoms
    only (that is, no modifiers, filters, or phrases).  It ignores
    any explicit groupings by the user, the "magical" floating
    subplan, any subplans that include filters or modifiers, and
    any nodes that have a mix of boolean operators between their
    atoms.
    
    This is probably more conservative that is strictly necessary,
    and phrases would likely be safe, but baby steps.
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/QueryParser.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/QueryParser.pm
index a4c74d1..7d3b86d 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/QueryParser.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/QueryParser.pm
@@ -3,6 +3,7 @@ use warnings;
 
 package QueryParser;
 use OpenSRF::Utils::JSON;
+use Data::Dumper;
 
 =head1 NAME
 
@@ -810,6 +811,8 @@ sub parse {
         $self->parse_tree( $self->floating_plan );
     }
 
+    warn "Query tree before pullup:\n" . Dumper($self->parse_tree) if $self->debug;
+    $self->parse_tree( $self->parse_tree->pullup );
     $self->parse_tree->plan_level(0);
 
     return $self;
@@ -1065,9 +1068,16 @@ sub decompose {
             $last_type = '';
         } elsif (/$$r{group_start_re}/) { # start of an explicit group
             warn '  'x$recursing."Encountered explicit group start\n" if $self->debug;
+
+            if ($last_type eq 'CLASS') {
+                warn '  'x$recursing."Previous class change generated an empty node. Removing...\n" if $self->debug;
+                $struct->remove_last_node;
+            }
+
             my $negate = $1;
             my ($substruct, $subremainder) = $self->decompose( $', $current_class, $recursing + 1 );
             $substruct->negate(1) if ($substruct && $negate);
+            $substruct->explicit(1) if ($substruct);
             $struct->add_node( $substruct ) if ($substruct);
             $_ = $subremainder;
             warn '  'x$recursing."Query remainder after bool group: $_\n" if $self->debug;
@@ -1090,11 +1100,11 @@ sub decompose {
             warn '  'x$recursing."RHS built\n" if $self->debug;
             warn '  'x$recursing."Post-AND remainder: $subremainder\n" if $self->debug;
 
-            my $wrapper = $self->new_plan( level => $recursing + 1 );
+            my $wrapper = $self->new_plan( level => $recursing + 1, joiner => '&'  );
 
             if ($LHS->floating) {
                 $wrapper->{query} = $LHS->{query};
-                my $outer_wrapper = $self->new_plan( level => $recursing + 1 );
+                my $outer_wrapper = $self->new_plan( level => $recursing + 1, joiner => '&'  );
                 $outer_wrapper->add_node($_) for ($wrapper,$RHS);
                 $LHS->{query} = [$outer_wrapper];
                 $struct = $LHS;
@@ -1507,6 +1517,199 @@ sub abstract_query2str_impl {
 
 #-------------------------------
 package QueryParser::query_plan;
+use Data::Dumper;
+$Data::Dumper::Indent = 0;
+
+sub atoms_only {
+    my $self = shift;
+    return @{$self->filters} == 0 &&
+            @{$self->modifiers} == 0 &&
+            @{[map { @{$_->phrases} } grep { ref($_) && $_->isa('QueryParser::query_plan::node')} @{$self->query_nodes}]} == 0
+    ;
+}
+
+sub _identical {
+    my( $left, $right ) = @_;
+    return 0 if scalar @$left != scalar @$right;
+    my %hash;
+    @hash{ @$left, @$right } = ();
+    return scalar keys %hash == scalar @$left;
+}
+
+sub pullup {
+    my $self = shift;
+    my $current_joiner = shift;
+
+    # burrow down until we our kids have no subqueries
+    my $downlink_joiner;
+    for my $qnode (@{ $self->query_nodes }) {
+        $downlink_joiner = $qnode if (!ref($qnode));
+        if (ref($qnode) && $qnode->can('pullup')) {
+            $qnode->pullup($downlink_joiner);
+        }
+    }
+
+    warn "Entering pullup depth ". $self->plan_level . "\n"
+        if $self->QueryParser->debug;
+
+    # PASS 1: loop, attempting to pull up simple nodes
+    my @new_nodes;
+    my $prev_node;
+    my $prev_op;
+
+    my $prev_joiner;
+
+    my $old_qnodes = $self->query_nodes; # we will ignore all but ::node objects in this list
+    warn @$old_qnodes . " plans at pullup depth ". $self->plan_level . "\n"
+        if $self->QueryParser->debug;
+
+    while (my $p = shift(@$old_qnodes)) {
+
+        # joiners and ::node's get pushed onto the stack of new nodes
+        if (!ref($p) or !$p->isa('QueryParser::query_plan')) {
+            push @new_nodes, $p;
+            next;
+        }
+
+        # keep explicit and floating plans
+        if ($p->explicit or $p->floating) {
+            push @new_nodes, $p;
+            next;
+        }
+
+        if ($p->atoms_only) {
+
+            # 1-node plans get pulled up regardless of the plan's joiner
+            if (@{$p->query_nodes} == 1) {
+                for my $a (@{$p->query_nodes}) {
+                    if (ref($a) and $a->can('plan')) {
+                        $a->plan($self);
+                    }
+                    push @new_nodes, $a;
+                }
+                next;
+            }
+
+            # gather the joiners
+            my %joiners = ( '&' => 0, '|' => 0 );
+            my @nodelist = @{$p->query_nodes};
+            while (my $n = shift(@nodelist)) {
+                next if ref($n); # only look at joiners
+                $joiners{$n}++;
+            }
+
+            if (!($joiners{'&'} > 0 and $joiners{'|'} > 0)) { # mix of joiners? stop
+                if ($joiners{$self->joiner} > 0) { # needs to be our joiner in use
+                    for my $a (@{$p->query_nodes}) {
+                        if (ref($a) and $a->can('plan')) {
+                            $a->plan($self);
+                        }
+                        push @new_nodes, $a;
+                    }
+                    next;
+                }
+            }
+        }
+
+        # default is to keep the whole plan
+        push @new_nodes, $p;
+    }
+                
+    warn @new_nodes . " nodes after pullup of simple nodes at depth ". $self->plan_level . "\n"
+        if $self->QueryParser->debug;
+
+
+    # PASS 2: merge adjacent ::node's
+    my $dangling = 0;
+    my $sync_node = $prev_joiner = undef;
+    $old_qnodes = [@new_nodes];
+    @new_nodes = ();
+    while ( my $n = shift(@$old_qnodes) ) {
+
+        # joiners
+        if (!ref($n)) {
+            $prev_joiner = $current_joiner;
+            $current_joiner = $n;
+            warn "Joiner, recording it. [$prev_joiner => $current_joiner]\n" if $self->QueryParser->debug;
+            next;
+        }
+
+        # ::plan's etc get pushed onto the stack of new nodes
+        if (!$n->isa('QueryParser::query_plan::node')) {
+            push @new_nodes, $current_joiner if (@new_nodes);
+            push @new_nodes, $n;
+            $sync_node = undef;
+            warn "Not a ::node, pushing onto the stack [$n]\n" if $self->QueryParser->debug;
+            next;
+        }
+
+        # grab the current target node
+        if (!$sync_node) {
+            warn "No sync_node, picking a new one\n" if $self->QueryParser->debug;
+            $sync_node = $n;
+            push @new_nodes, $current_joiner if (@new_nodes);
+            push @new_nodes, $n;
+            next;
+        }
+
+        if (@{$n->query_atoms} == 0) {
+            warn "weird ... empty node ...skipping\n" if $self->QueryParser->debug;
+            push @new_nodes, $current_joiner if (@new_nodes);
+            shift @$old_qnodes;
+            next;
+        }
+
+        my $sync_joiner = $sync_node->effective_joiner;
+        my $n_joiner = $n->effective_joiner;
+
+        # plans of a different class or field set stay where they are
+        if ($sync_node->classname ne $n->classname or !_identical($sync_node->fields,$n->fields)) {
+            warn "Class/Field change! Need a new sync_node\n" if $self->QueryParser->debug;
+            push @new_nodes, $current_joiner;
+            push @new_nodes, $n;
+            $sync_node = $n;
+            $dangling = 1;
+            next;
+        }
+
+        if (!$sync_joiner or !$n_joiner) { # a node has a mix ... can't merge either
+            warn "Mixed joiners, need a new sync_node\n" if $self->QueryParser->debug;
+            push @new_nodes, $current_joiner;
+            push @new_nodes, $n;
+            $sync_node = $n;
+            $dangling = 1;
+            next;
+        } elsif ($sync_joiner ne $n_joiner) { # different joiners, can't merge
+            warn "Differing joiners, need a new sync_node\n" if $self->QueryParser->debug;
+            push @new_nodes, $current_joiner;
+            push @new_nodes, $n;
+            $sync_node = $n;
+            $dangling = 1;
+            next;
+        }
+
+        # we can push the next ::node's atoms onto our stack
+        push @{$sync_node->query_atoms}, $current_joiner;
+        for my $a (@{$n->query_atoms}) {
+            if (ref($a)) {
+                $a->{node} = $sync_node;
+            }
+            push @{$sync_node->query_atoms}, $a;
+        }
+
+        warn "Merged ".@{$n->query_atoms}." atoms into sync_node\n" if $self->QueryParser->debug;
+        $dangling = 0;
+
+    }
+
+    push @new_nodes, $sync_node if ($dangling && $sync_node != $new_nodes[-1]);
+   
+    warn @new_nodes . " nodes at pullup depth ". $self->plan_level . " after compression\n"
+        if $self->QueryParser->debug;
+
+    $self->{query} = \@new_nodes;
+    return $self;
+}
 
 sub QueryParser {
     my $self = shift;
@@ -1693,6 +1896,13 @@ sub floating {
     return $self->{floating};
 }
 
+sub explicit {
+    my $self = shift;
+    my $f = shift;
+    $self->{explicit} = $f if (defined $f);
+    return $self->{explicit};
+}
+
 sub add_node {
     my $self = shift;
     my $node = shift;
@@ -1854,6 +2064,27 @@ package QueryParser::query_plan::node;
 use Data::Dumper;
 $Data::Dumper::Indent = 0;
 
+sub effective_joiner {
+    my $node = shift;
+
+    my @nodelist = @{$node->query_atoms};
+    return $node->plan->joiner if (@nodelist == 1);
+
+    # gather the joiners
+    my %joiners = ( '&' => 0, '|' => 0 );
+    while (my $n = shift(@nodelist)) {
+        next if ref($n); # only look at joiners
+        $joiners{$n}++;
+    }
+
+    if (!($joiners{'&'} > 0 and $joiners{'|'} > 0)) { # no mix of joiners
+        return '|' if ($joiners{'|'});
+        return '&';
+    }
+
+    return undef;
+}
+
 sub new {
     my $pkg = shift;
     $pkg = ref($pkg) || $pkg;

commit fc8af9f703333ba9b963854f95e3755b9c838f08
Author: Mike Rylander <mrylander at gmail.com>
Date:   Thu Oct 20 10:19:09 2016 -0400

    LP#1005040: Add more ignorable filters to the sidebar count calculation
    
    We need to ignore site() and location_groups() when counting filters, as
    these have widgets in the search bar.
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/src/templates/opac/parts/searchbar.tt2 b/Open-ILS/src/templates/opac/parts/searchbar.tt2
index fd86758..97517c0 100644
--- a/Open-ILS/src/templates/opac/parts/searchbar.tt2
+++ b/Open-ILS/src/templates/opac/parts/searchbar.tt2
@@ -3,7 +3,7 @@
 
 # We need to ignore some filters in our count
 
-fignore = ['core_limit','limit','badge_orgs','badges','estimation_strategy','depth'];
+fignore = ['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;

commit 6ebf34a1e18f1bd6215567a9b51b94d0bd24c35b
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Tue Oct 18 14:20:25 2016 -0400

    LP#1005040: adjust test cases
    
    * Now reflects change in signature of
      OpenILS::WWW::EGCatLoader::_prepare_biblio_search()
    * add test case for change in date filter mapping
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/perlmods/t/19-OpenILS-WWW-EGCatLoader.t b/Open-ILS/src/perlmods/t/19-OpenILS-WWW-EGCatLoader.t
index 9d33829..e8c1a0b 100644
--- a/Open-ILS/src/perlmods/t/19-OpenILS-WWW-EGCatLoader.t
+++ b/Open-ILS/src/perlmods/t/19-OpenILS-WWW-EGCatLoader.t
@@ -1,6 +1,6 @@
 #!perl -T
 
-use Test::More tests => 9;
+use Test::More tests => 11;
 use CGI;
 
 BEGIN {
@@ -17,7 +17,14 @@ my $cgi = CGI->new();
 $cgi->param('query', 'sort(titlesort) cats site(CONS)');
 $cgi->param('sort',  '');
 $cgi->param('depth', 0);
-my ($new_query, $site, $depth) = OpenILS::WWW::EGCatLoader::_prepare_biblio_search($cgi, $ctx);
+my ($user_query, $query, $site, $depth) = OpenILS::WWW::EGCatLoader::_prepare_biblio_search($cgi, $ctx);
+is($user_query, 'sort(titlesort) cats site(CONS)', 'LP#100504: user query left as is');
 is($site,  'CONS', 'successfully parsed site');
 is($depth, '0',    'successfully parsed depth');
-is($new_query,  'cats site(CONS) depth(0)', 'LP#1562153: change sort order to relevance');
+is($query,  'cats site(CONS) depth(0)', 'LP#1562153: change sort order to relevance');
+
+# test date filter
+$cgi->param('pubdate', 'is');
+$cgi->param('date1', '1999');
+($user_query, $query, $site, $depth) = OpenILS::WWW::EGCatLoader::_prepare_biblio_search($cgi, $ctx);
+is($query, 'date1(1999)  cats site(CONS) depth(0)', 'LP#1005040: "is" pubdate filter mapped to date1() filter');

commit 3a40d740494eca897efb17c0a0ac48428855b7c1
Author: Mike Rylander <mrylander at gmail.com>
Date:   Tue Oct 11 11:36:42 2016 -0400

    LP#1005040: show number of filters applied
    
    If at least advanced search filter is applied, the number
    of them in force is displayed just below the search bar
    next to the refine search link.
    
    This patch also renames "opac-auto-102" CSS class to "refine_search"
    and tweaks its left padding.
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/opac/css/style.css.tt2 b/Open-ILS/src/templates/opac/css/style.css.tt2
index a6ba64e..77b3b62 100644
--- a/Open-ILS/src/templates/opac/css/style.css.tt2
+++ b/Open-ILS/src/templates/opac/css/style.css.tt2
@@ -51,6 +51,11 @@ a {
     margin-left: 1em;
 }
 
+.refine_search {
+    padding-bottom: 7px;
+    margin-left: 1em;
+}
+
 /*
 #search-wrapper select {
     border:0px solid [% css_colors.border_dark %];
diff --git a/Open-ILS/src/templates/opac/parts/searchbar.tt2 b/Open-ILS/src/templates/opac/parts/searchbar.tt2
index 2c162fe..fd86758 100644
--- a/Open-ILS/src/templates/opac/parts/searchbar.tt2
+++ b/Open-ILS/src/templates/opac/parts/searchbar.tt2
@@ -1,5 +1,18 @@
 <h3 class="sr-only">[% l('Catalog Search') %]</h3>
-[% PROCESS "opac/parts/org_selector.tt2" %]
+[% PROCESS "opac/parts/org_selector.tt2";
+
+# We need to ignore some filters in our count
+
+fignore = ['core_limit','limit','badge_orgs','badges','estimation_strategy','depth'];
+fcount = 0;
+FOR f IN ctx.query_struct.filters;
+    IF fignore.grep('^' _ f.name _ '$').size;
+        NEXT;
+    END;
+    fcount = fcount + 1;
+END;
+
+ %]
 <div id="search-wrapper">
     [% UNLESS took_care_of_form -%]
     <form action="[% ctx.opac_root %]/results" method="get">
@@ -113,12 +126,20 @@
         </form>
     [% END %]
     [% IF (is_advanced AND NOT is_special) AND CGI.param('qtype') %]
-    <div class="opac-auto-102">
+    <div class="refine_search">
+        [% IF fcount > 0 %]
+        [ [% l('[quant,_1,filter,filters] applied', fcount) %] ]
+        [% END %]
         [ <a href="[% mkurl(ctx.opac_root _ '/advanced') %]">[%
             l('Refine My Original Search')
         %]</a> ]
     </div>
     [% END %]
+    <!-- Canonicalized query:
+
+    [% ctx.canonicalized_query | html %]
+
+    -->
     <!--
     <div id="breadcrumb">
         <a href="[% ctx.opac_root %]/home">[% l('Catalog Home') %]</a> >
diff --git a/Open-ILS/web/css/skin/default/opac/semiauto.css b/Open-ILS/web/css/skin/default/opac/semiauto.css
index 0770599..5ce8dad 100644
--- a/Open-ILS/web/css/skin/default/opac/semiauto.css
+++ b/Open-ILS/web/css/skin/default/opac/semiauto.css
@@ -12,7 +12,6 @@
 }
 .pad-bottom-five { padding: 5px; }
 .item_list_padding { padding: 8px 0px 6px 0px; border: 0; }
-.opac-auto-102 { padding-bottom: 7px; }
 .opac-auto-108 { padding-left: 5px; }
 .pad-top-ten { padding-top: 10px; }
 .opac-auto-121 { padding-top: 6px; }

commit 3d917fb818193f409d03845eb1b63b3d6956399f
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Tue Oct 4 17:00:15 2016 -0400

    LP#1005040: add filter control widget for publication year
    
    This patch also changes the rewriting of an "is"
    pubdate filter from between(value,value) to date1(value),
    which should be slightly faster.
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

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 d234753..c5a5307 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm
@@ -108,8 +108,7 @@ sub _prepare_biblio_search {
             $btw .= ')';
             $query = "$btw $query";
         } elsif ($cgi->param('pubdate') eq 'is') {
-            $query = 'between(' . $cgi->param('date1') .
-                ',' .  $cgi->param('date1') . ") $query";  # sic, date1 twice
+            $query = 'date1(' . $cgi->param('date1') . ") $query";
         } else {
             $query = $cgi->param('pubdate') .
                 '(' . $cgi->param('date1') . ") $query";
diff --git a/Open-ILS/src/templates/opac/parts/result/adv_filter.tt2 b/Open-ILS/src/templates/opac/parts/result/adv_filter.tt2
index ca3cbf0..dbe712f 100644
--- a/Open-ILS/src/templates/opac/parts/result/adv_filter.tt2
+++ b/Open-ILS/src/templates/opac/parts/result/adv_filter.tt2
@@ -5,6 +5,8 @@
 # as that's got its own widget
 ignore_filters = ['search_format'];
 
+pubdate_filters = ['date1', 'before', 'after', 'between'];
+
 FOR filter IN ctx.query_struct.filters;
     fname = filter.name;
     IF ignore_filters.grep('^' _ fname _ '$').size;
@@ -14,7 +16,8 @@ FOR filter IN ctx.query_struct.filters;
     fvalues = filter.args;
     crad = ctx.get_crad(fname);
 
-    IF crad; # will be some special ones, like locations
+    # will be some special ones, like locations
+    IF crad AND NOT pubdate_filters.grep('^' _ filter.name _ '$').size;
         remove_filter = 'fi:' _ fname;
 %]
     <div class="facet_box_temp filter_box_temp">
@@ -75,7 +78,33 @@ FOR filter IN ctx.query_struct.filters;
             </div>
         </div> <!-- box_wrapper -->
     </div> <!-- facet_box_temp -->
-    [% END; # IF locations %]
+[% END; # IF locations %]
+
+[% IF pubdate_filters.grep('^' _ filter.name _ '$').size;
+    date1 = CGI.param('date1');
+    date2 = CGI.param('date2');
+%]
+    <div class="facet_box_temp filter_box_temp">
+        <div class="header">
+            <a class="button"
+              title="[% l('Remove publication date filter') %]"
+              href="[% mkurl('', {}, ['pubdate', 'date1', 'date2']) %]" rel="nofollow" vocab="">
+              [% l("Remove") %]
+            </a>
+            <h4 class="title">[% IF filter.negate; l('Not'); END %] [% l('Publication Year') %]</h4>
+        </div>
+        <div class="box_wrapper">
+            <div class="box">
+              [% IF    filter.name == 'date1'      %][% l('[_1]', date1) %]
+              [% ELSIF filter.name == 'before'  %][% l('Before [_1]', date1) %]
+              [% ELSIF filter.name == 'after'   %][% l('After [_1]', date1) %]
+              [% ELSIF filter.name == 'between' %][% l('Between [_1] and [_2]', date1, date2) %]
+              [% END %]
+            </div>
+        </div> <!-- box_wrapper -->
+    </div> <!-- facet_box_temp -->
+[% END; # IF pubdate_filters %]
+
 [% END; # FOR %]
 </div> <!-- facet_box_wrapper -->
 

commit a341d347ded57a7bb0a4e11f980486e9809bbfd7
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Tue Oct 4 16:06:32 2016 -0400

    LP#1005040: teach filter boxes about human-readable crad descriptions
    
    This patch ensures that the human-readable record attribute
    description, if available, will be used for the title of each
    filter control widget.  It also gives descriptions to record
    attributes commonly used for advanced search filters.
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
index 5e76959..bfff3e2 100644
--- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql
+++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
@@ -6321,8 +6321,8 @@ INSERT INTO config.marc21_ff_pos_map (fixed_field, tag, rec_type,start_pos, leng
 
 -- record attributes
 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('alph','Alph','Alph');
-INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('audience','Audn','Audn');
-INSERT INTO config.record_attr_definition (name,label,fixed_field,multi) values ('bib_level','BLvl','BLvl',FALSE);
+INSERT INTO config.record_attr_definition (name,label,fixed_field,description) values ('audience','Audn','Audn', oils_i18n_gettext('audience', 'Audience', 'crad', 'label'));
+INSERT INTO config.record_attr_definition (name,label,fixed_field,multi,description) values ('bib_level','BLvl','BLvl',FALSE,oils_i18n_gettext('bib_level', 'Bib Level', 'crad', 'label'));
 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('biog','Biog','Biog');
 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('conf','Conf','Conf');
 INSERT INTO config.record_attr_definition (name,label,fixed_field,multi) values ('control_type','Ctrl','Ctrl',FALSE);
@@ -6334,18 +6334,18 @@ INSERT INTO config.record_attr_definition (name,label,fixed_field,multi) values
 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('pub_status','DtSt','DtSt');
 INSERT INTO config.record_attr_definition (name,label,fixed_field,multi) values ('enc_level','ELvl','ELvl',FALSE);
 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('fest','Fest','Fest');
-INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('item_form','Form','Form');
+INSERT INTO config.record_attr_definition (name,label,fixed_field,description) values ('item_form','Form','Form',oils_i18n_gettext('item_form', 'Item Form', 'crad', 'label'));
 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('gpub','GPub','GPub');
 INSERT INTO config.record_attr_definition (name,label,fixed_field,composite) values ('ills','Ills','Ills',TRUE);
 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('indx','Indx','Indx');
-INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('item_lang','Lang','Lang');
+INSERT INTO config.record_attr_definition (name,label,fixed_field,description) values ('item_lang','Lang','Lang',oils_i18n_gettext('item_lang', 'Language', 'crad', 'label'));
 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('language','Language (2.0 compat version)','Lang');
-INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('lit_form','LitF','LitF');
+INSERT INTO config.record_attr_definition (name,label,fixed_field,description) values ('lit_form','LitF','LitF',oils_i18n_gettext('lit_form', 'Literary Form', 'crad', 'label'));
 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('mrec','MRec','MRec');
 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('ff_sl','S/L','S/L');
 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('type_mat','TMat','TMat');
-INSERT INTO config.record_attr_definition (name,label,fixed_field,multi) values ('item_type','Type','Type',FALSE);
-INSERT INTO config.record_attr_definition (name,label,phys_char_sf) values ('vr_format','Videorecording format',72);
+INSERT INTO config.record_attr_definition (name,label,fixed_field,multi,description) values ('item_type','Type','Type',FALSE,oils_i18n_gettext('item_type', 'Item Type', 'crad', 'label'));
+INSERT INTO config.record_attr_definition (name,label,phys_char_sf,description) values ('vr_format','Videorecording format',72,oils_i18n_gettext('vr_format', 'Video Format', 'crad', 'label'));
 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('file','File','File');
 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('freq','Freq','Freq');
 INSERT INTO config.record_attr_definition (name,label,fixed_field) values ('regl','Regl','Regl');
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.give-crad-human-descriptions.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.give-crad-human-descriptions.sql
new file mode 100644
index 0000000..ee52abd
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.give-crad-human-descriptions.sql
@@ -0,0 +1,32 @@
+BEGIN;
+
+UPDATE config.record_attr_definition
+SET description = oils_i18n_gettext('audience', 'Audience', 'crad', 'label')
+WHERE description IS NULL
+AND name = 'audience';
+UPDATE config.record_attr_definition
+SET description = oils_i18n_gettext('bib_level', 'Bib Level', 'crad', 'label')
+WHERE description IS NULL
+AND name = 'bib_level';
+UPDATE config.record_attr_definition
+SET description = oils_i18n_gettext('item_form', 'Item Form', 'crad', 'label')
+WHERE description IS NULL
+AND name = 'item_form';
+UPDATE config.record_attr_definition
+SET description = oils_i18n_gettext('item_lang', 'Language', 'crad', 'label')
+WHERE description IS NULL
+AND name = 'item_lang';
+UPDATE config.record_attr_definition
+SET description = oils_i18n_gettext('lit_form', 'Literary Form', 'crad', 'label')
+WHERE description IS NULL
+AND name = 'lit_form';
+UPDATE config.record_attr_definition
+SET description = oils_i18n_gettext('item_type', 'Item Type', 'crad', 'label')
+WHERE description IS NULL
+AND name = 'item_type';
+UPDATE config.record_attr_definition
+SET description = oils_i18n_gettext('vr_format', 'Video Format', 'crad', 'label')
+WHERE description IS NULL
+AND name = 'vr_format';
+
+COMMIT;
diff --git a/Open-ILS/src/templates/opac/parts/result/adv_filter.tt2 b/Open-ILS/src/templates/opac/parts/result/adv_filter.tt2
index a70bd29..ca3cbf0 100644
--- a/Open-ILS/src/templates/opac/parts/result/adv_filter.tt2
+++ b/Open-ILS/src/templates/opac/parts/result/adv_filter.tt2
@@ -20,11 +20,11 @@ FOR filter IN ctx.query_struct.filters;
     <div class="facet_box_temp filter_box_temp">
         <div class="header">
             <a class="button"
-              title="[% l('Remove [_1] filter', crad.label) %]"
+              title="[% l('Remove [_1] filter', (crad.description || crad.label)) %]"
               href="[% mkurl('', {}, [remove_filter]) %]" rel="nofollow" vocab="">
               [% l("Remove") %]
             </a>
-            <h4 class="title">[% IF filter.negate; l('Not'); END %] [% crad.label | html %]</h4>
+            <h4 class="title">[% IF filter.negate; l('Not'); END %] [% (crad.description || crad.label) | html %]</h4>
         </div>
         <div class="box_wrapper">
             <div class="box">

commit 3755e827644ea52da5bfed0289ed900ee9ecf1ab
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Tue Oct 4 14:21:29 2016 -0400

    LP#1005040: display search filter sidebar on lowhits page
    
    With this, if a user over-filters their initial search, they
    can more easily remove filters.
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/opac/parts/result/lowhits.tt2 b/Open-ILS/src/templates/opac/parts/result/lowhits.tt2
index 5a69351..cbaf0f2 100644
--- a/Open-ILS/src/templates/opac/parts/result/lowhits.tt2
+++ b/Open-ILS/src/templates/opac/parts/result/lowhits.tt2
@@ -1,5 +1,9 @@
 <div>
     <div id="zero_search_hits">
+        <div class="facet_sidebar_hidden" id="facet_sidebar">
+          <h3 class="sr-only">[% l('Search Results filters') %]</h3>
+          [% INCLUDE 'opac/parts/result/adv_filter.tt2' %]
+        </div>
         <div class="zero_search_hits_saved">
             [% INCLUDE "opac/parts/staff_saved_searches.tt2" %]
         </div>

commit c6cb2c8a7f15f965c89ea676adbf6f8423824df8
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Tue Oct 4 14:08:14 2016 -0400

    LP#1005040: various improvements to the filter control widgets
    
    * ensure that filter values are sorted
    * fix the link for removing location filters
    * don't display a filter box for search_format, as that
      already has a separate drop-down in the search bar
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/opac/parts/result/adv_filter.tt2 b/Open-ILS/src/templates/opac/parts/result/adv_filter.tt2
index 6de6b2d..a70bd29 100644
--- a/Open-ILS/src/templates/opac/parts/result/adv_filter.tt2
+++ b/Open-ILS/src/templates/opac/parts/result/adv_filter.tt2
@@ -1,10 +1,15 @@
 <div class="facet_box_wrapper filter_box_wrapper">
 [%
 
-ignore_filters = ['sort','statuses','site'];
+# don't display a box for the search_format filter,
+# as that's got its own widget
+ignore_filters = ['search_format'];
 
 FOR filter IN ctx.query_struct.filters;
     fname = filter.name;
+    IF ignore_filters.grep('^' _ fname _ '$').size;
+      NEXT;
+    END;
 
     fvalues = filter.args;
     crad = ctx.get_crad(fname);
@@ -23,16 +28,21 @@ FOR filter IN ctx.query_struct.filters;
         </div>
         <div class="box_wrapper">
             <div class="box">
-            [% FOR fval IN fvalues;
+            [% temp = [];
+               FOR fval IN fvalues;
                 thing = ctx.search_ccvm('ctype',fname,'code',fval).0;
                 display_value = thing.search_label || thing.value;
-                IF display_value.defined; %]
+                IF display_value.defined;
+                 temp.push(display_value);
+                END;
+               END;
+               FOR display_value IN temp.sort;
+            %]
                     <div class="facet_template filter_template">
                         <div class="facet filter">
                               [% display_value | html%]
                         </div>
                     </div>
-            [%  END; # IF %]
             [% END; # FOR %]
             </div>
         </div> <!-- box_wrapper -->
@@ -44,17 +54,21 @@ FOR filter IN ctx.query_struct.filters;
         <div class="header">
             <a class="button"
               title="[% l('Remove location filter') %]"
-              href="[% mkurl('', {}, ['locations']) %]" rel="nofollow" vocab="">
+              href="[% mkurl('', {}, ['fi:locations']) %]" rel="nofollow" vocab="">
               [% l("Remove") %]
             </a>
             <h4 class="title">[% IF filter.negate; l('Not'); END %] [% l('Locations') %]</h4>
         </div>
         <div class="box_wrapper">
             <div class="box">
-            [% FOR loc IN locs %]
+            [% temp = [];
+               FOR loc IN locs;
+                temp.push(loc.name);
+               END;
+               FOR display_name IN temp.sort; %]
                 <div class="facet_template filter_template">
                   <div class="facet filter">
-                    [% loc.name | html%]
+                    [% display_name | html%]
                   </div>
                 </div>
             [% END; # FOR %]

commit fe51c342c8aeab90b98df5bf33a5e4a7400cac54
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Tue Oct 4 12:25:42 2016 -0400

    LP#1005040: CSS styling of filter control boxes
    
    This patch adds several CSS classes to support
    distinguishing filter boxes from facet boxes:
    
    filter_box_wrapper
    filter_box_temp
    filter_template
    filter
    
    It also sets a different background color for the header of filter
    boxes. Padding between entries in a filter list is reduced as
    compared to facets, both to save a bit of vertical space and to
    subtly distinguish filters from facets.
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/templates/opac/css/style.css.tt2 b/Open-ILS/src/templates/opac/css/style.css.tt2
index 8a3c4f7..a6ba64e 100644
--- a/Open-ILS/src/templates/opac/css/style.css.tt2
+++ b/Open-ILS/src/templates/opac/css/style.css.tt2
@@ -1064,6 +1064,10 @@ div#facet_sidebar {
     padding-top:4px;
 }
 
+.facet_box_temp.filter_box_temp .header {
+    background: [% css_colors.background_invert %] !important;
+}
+
 .facet_box_temp .header .title {
     float:left;
     padding-top:6px;
@@ -1106,6 +1110,10 @@ div#facet_sidebar {
     padding: 2px;
 }
 
+.facet_template.filter_template div {
+    padding: 0px !important;
+}
+
 .facet_template .count {
     text-align: right;
     color: [% css_colors.accent_mediumdark %];
diff --git a/Open-ILS/src/templates/opac/parts/result/adv_filter.tt2 b/Open-ILS/src/templates/opac/parts/result/adv_filter.tt2
index 9e7bd14..6de6b2d 100644
--- a/Open-ILS/src/templates/opac/parts/result/adv_filter.tt2
+++ b/Open-ILS/src/templates/opac/parts/result/adv_filter.tt2
@@ -1,4 +1,4 @@
-<div class="facet_box_wrapper">
+<div class="facet_box_wrapper filter_box_wrapper">
 [%
 
 ignore_filters = ['sort','statuses','site'];
@@ -12,7 +12,7 @@ FOR filter IN ctx.query_struct.filters;
     IF crad; # will be some special ones, like locations
         remove_filter = 'fi:' _ fname;
 %]
-    <div class="facet_box_temp">
+    <div class="facet_box_temp filter_box_temp">
         <div class="header">
             <a class="button"
               title="[% l('Remove [_1] filter', crad.label) %]"
@@ -27,8 +27,8 @@ FOR filter IN ctx.query_struct.filters;
                 thing = ctx.search_ccvm('ctype',fname,'code',fval).0;
                 display_value = thing.search_label || thing.value;
                 IF display_value.defined; %]
-                    <div class="facet_template">
-                        <div class="facet">
+                    <div class="facet_template filter_template">
+                        <div class="facet filter">
                               [% display_value | html%]
                         </div>
                     </div>
@@ -40,7 +40,7 @@ FOR filter IN ctx.query_struct.filters;
     [% END; # IF crad %]
 
 [%  IF filter.name == 'locations'; locs = ctx.search_acpl('id',filter.args) %]
-    <div class="facet_box_temp">
+    <div class="facet_box_temp filter_box_temp">
         <div class="header">
             <a class="button"
               title="[% l('Remove location filter') %]"
@@ -52,8 +52,8 @@ FOR filter IN ctx.query_struct.filters;
         <div class="box_wrapper">
             <div class="box">
             [% FOR loc IN locs %]
-                <div class="facet_template">
-                  <div class="facet">
+                <div class="facet_template filter_template">
+                  <div class="facet filter">
                     [% loc.name | html%]
                   </div>
                 </div>

commit 743cb27c2a005adbde5488e5c877d3ad7e96872c
Author: Mike Rylander <mrylander at gmail.com>
Date:   Tue Sep 20 17:09:35 2016 -0400

    LP#1005040: add filter control widgets to TPAC
    
    This patch also moves facet retrieval to after record retrieval, to
    make sure facet data is available, and wait for it
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm
index f595c28..014d250 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm
@@ -16,7 +16,7 @@ use OpenSRF::Utils::Logger qw/:logger/;
 
 use OpenSRF::Utils::JSON;
 
-use Time::HiRes qw(time);
+use Time::HiRes qw(time sleep);
 use OpenSRF::EX qw(:try);
 use Digest::MD5 qw(md5_hex);
 
@@ -1142,7 +1142,7 @@ sub staged_search {
     $method .= '.staff' if $self->api_name =~ /staff$/;
     $method .= '.atomic';
                 
-    if (!$search_hash{query}) {
+    if (!$search_hash->{query}) {
         return {count => 0} unless (
             $search_hash and 
             $search_hash->{searches} and 
@@ -1407,6 +1407,14 @@ sub retrieve_cached_facets {
 
     return undef unless ($key and $key =~ /_facets$/);
 
+    eval {
+        local $SIG{ALARM} = sub {die};
+        alarm(2); # we'll sleep for as much as 2s
+        do {
+            die if $cache->get_cache($key . '_COMPLETE');
+        } while (sleep(0.05));
+    };
+
     my $blob = $cache->get_cache($key) || {};
 
     my $facets = {};
@@ -1477,6 +1485,7 @@ sub cache_facets {
     $logger->info("facet compilation: cached with key=$key");
 
     $cache->put_cache($key, $data, $cache_timeout);
+    $cache->put_cache($key.'_COMPLETE', 1, $cache_timeout);
 }
 
 sub cache_staged_search_page {
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 48c218a..d234753 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm
@@ -69,7 +69,9 @@ sub _prepare_biblio_search_basics {
 sub _prepare_biblio_search {
     my ($cgi, $ctx) = @_;
 
-    my $query = _prepare_biblio_search_basics($cgi) || '';
+    # XXX This will still contain the jtitle hack...
+    my $user_query = _prepare_biblio_search_basics($cgi) || '';
+    my $query = $user_query;
 
     $query .= ' ' . $ctx->{global_search_filter} if $ctx->{global_search_filter};
 
@@ -189,9 +191,9 @@ sub _prepare_biblio_search {
         return $query;
     };
 
-    $logger->info("tpac: site=$site, depth=$depth, query=$query");
+    $logger->info("tpac: site=$site, depth=$depth, user_query=$user_query, query=$query");
 
-    return ($query, $site, $depth);
+    return ($user_query, $query, $site, $depth);
 }
 
 sub _get_search_limit {
@@ -389,7 +391,7 @@ sub load_rresults {
         $offset = 0;
     }
 
-    my ($query, $site, $depth) = _prepare_biblio_search($cgi, $ctx);
+    my ($user_query, $query, $site, $depth) = _prepare_biblio_search($cgi, $ctx);
 
     $self->get_staff_search_settings;
 
@@ -430,6 +432,7 @@ sub load_rresults {
         }
 
         # Stuff these into the TT context so that templates can use them in redrawing forms
+        $ctx->{user_query} = $user_query;
         $ctx->{processed_search_query} = $query;
 
         $query = "$_ $query" for @facets;
@@ -461,7 +464,10 @@ sub load_rresults {
 
     $ctx->{ids} = $rec_ids;
     $ctx->{hit_count} = $results->{count};
-    $ctx->{parsed_query} = $results->{parsed_query};
+    $ctx->{query_struct} = $results->{global_summary}{query_struct};
+    $logger->debug('query struct: '. Dumper($ctx->{query_struct}));
+    $ctx->{canonicalized_query} = $results->{global_summary}{canonicalized_query};
+    $ctx->{search_summary} = $results->{global_summary};
 
     if ($find_last) {
         # redirect to the record detail page for the last record in the results
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 fdf51f7..cf1f7e9 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm
@@ -46,6 +46,7 @@ sub child_init {
         $ro_object_subs->{$locale}->{aou_tree}();
         $ro_object_subs->{$locale}->{aouct_tree}();
         $ro_object_subs->{$locale}->{ccvm_list}();
+        $ro_object_subs->{$locale}->{crad_list}();
         $ro_object_subs->{$locale}->{get_authority_fields}(1);
     }
 }
@@ -109,17 +110,28 @@ sub init_ro_object_cache {
             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}} ];
             }
-            #$cache{search}{$locale}{$hint}{$field} = {} unless $cache{search}{$locale}{$hint}{$field};
+
             my $e = new_editor();
             $cache{search}{$locale}{$hint}{$field}{$cacheval} = $e->$method($search_obj)
                 unless $cache{search}{$locale}{$hint}{$field}{$cacheval};
@@ -453,20 +465,19 @@ sub get_records_and_facets {
         }
     }
 
-
-    $self->timelog("get_records_and_facets():almost ready to fetch facets");
-    # collect the facet data
-    my $search = OpenSRF::AppSession->create('open-ils.search');
-    my $facet_req = $search->request(
-        'open-ils.search.facet_cache.retrieve', $facet_key
-    ) if $facet_key;
-
     # gather up the unapi recs
     $ses->session_wait(1);
     $self->timelog("get_records_and_facets():past session wait");
 
     my $facets = {};
     if ($facet_key) {
+        $self->timelog("get_records_and_facets():almost ready to fetch facets");
+        # collect the facet data
+        my $search = OpenSRF::AppSession->create('open-ils.search');
+        my $facet_req = $search->request(
+            'open-ils.search.facet_cache.retrieve', $facet_key
+        );
+
         my $tmp_facets = $facet_req->gather(1);
         $self->timelog("get_records_and_facets(): gathered facet data");
         for my $cmf_id (keys %$tmp_facets) {
@@ -490,12 +501,11 @@ sub get_records_and_facets {
             }
         }
         $self->timelog("get_records_and_facets(): gathered/sorted facet data");
+        $search->kill_me;
     } else {
         $facets = undef;
     }
 
-    $search->kill_me;
-
     return ($facets, map { $tmp_data{$_} } @$rec_ids);
 }
 
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/SuperCat.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/SuperCat.pm
index b4ba2d7..a599706 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/SuperCat.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/SuperCat.pm
@@ -537,7 +537,7 @@ sub unapi {
     $supercat->session_locale($locale);
 
     my $flesh_feed = parse_feed_type($format);
-    (my $base_format = $format) =~ s/(-full|-uris)$//o;
+    ($base_format = $format) =~ s/(-full|-uris)$//o;
     my ($id,$type,$command,$lib,$depth,$paging) = ('','record','');
     my $body = "Content-type: application/xml; charset=utf-8\n\n";
 
@@ -1443,9 +1443,9 @@ sub opensearch_feed {
     $safe_terms =~ s{'}{ }go;
     
     my $query_terms = 'site('.$org_unit->[0]->shortname.") $safe_terms";
-    $query_tems = "sort($sort) $query_terms" if ($sort);
-    $query_tems = "language($lang) $query_terms" if ($lang);
-    $query_tems = "#$sortdir $query_terms" if ($sortdir);
+    $query_terms = "sort($sort) $query_terms" if ($sort);
+    $query_terms = "language($lang) $query_terms" if ($lang);
+    $query_terms = "#$sortdir $query_terms" if ($sortdir);
 
     my $recs = $search->request(
         'open-ils.search.biblio.multiclass.query' => {
diff --git a/Open-ILS/src/templates/opac/parts/result/adv_filter.tt2 b/Open-ILS/src/templates/opac/parts/result/adv_filter.tt2
new file mode 100644
index 0000000..9e7bd14
--- /dev/null
+++ b/Open-ILS/src/templates/opac/parts/result/adv_filter.tt2
@@ -0,0 +1,67 @@
+<div class="facet_box_wrapper">
+[%
+
+ignore_filters = ['sort','statuses','site'];
+
+FOR filter IN ctx.query_struct.filters;
+    fname = filter.name;
+
+    fvalues = filter.args;
+    crad = ctx.get_crad(fname);
+
+    IF crad; # will be some special ones, like locations
+        remove_filter = 'fi:' _ fname;
+%]
+    <div class="facet_box_temp">
+        <div class="header">
+            <a class="button"
+              title="[% l('Remove [_1] filter', crad.label) %]"
+              href="[% mkurl('', {}, [remove_filter]) %]" rel="nofollow" vocab="">
+              [% l("Remove") %]
+            </a>
+            <h4 class="title">[% IF filter.negate; l('Not'); END %] [% crad.label | html %]</h4>
+        </div>
+        <div class="box_wrapper">
+            <div class="box">
+            [% FOR fval IN fvalues;
+                thing = ctx.search_ccvm('ctype',fname,'code',fval).0;
+                display_value = thing.search_label || thing.value;
+                IF display_value.defined; %]
+                    <div class="facet_template">
+                        <div class="facet">
+                              [% display_value | html%]
+                        </div>
+                    </div>
+            [%  END; # IF %]
+            [% END; # FOR %]
+            </div>
+        </div> <!-- box_wrapper -->
+    </div> <!-- facet_box_temp -->
+    [% END; # IF crad %]
+
+[%  IF filter.name == 'locations'; locs = ctx.search_acpl('id',filter.args) %]
+    <div class="facet_box_temp">
+        <div class="header">
+            <a class="button"
+              title="[% l('Remove location filter') %]"
+              href="[% mkurl('', {}, ['locations']) %]" rel="nofollow" vocab="">
+              [% l("Remove") %]
+            </a>
+            <h4 class="title">[% IF filter.negate; l('Not'); END %] [% l('Locations') %]</h4>
+        </div>
+        <div class="box_wrapper">
+            <div class="box">
+            [% FOR loc IN locs %]
+                <div class="facet_template">
+                  <div class="facet">
+                    [% loc.name | html%]
+                  </div>
+                </div>
+            [% END; # FOR %]
+            </div>
+        </div> <!-- box_wrapper -->
+    </div> <!-- facet_box_temp -->
+    [% END; # IF locations %]
+[% END; # FOR %]
+</div> <!-- facet_box_wrapper -->
+
diff --git a/Open-ILS/src/templates/opac/parts/result/table.tt2 b/Open-ILS/src/templates/opac/parts/result/table.tt2
index 02fe684..7e47daf 100644
--- a/Open-ILS/src/templates/opac/parts/result/table.tt2
+++ b/Open-ILS/src/templates/opac/parts/result/table.tt2
@@ -25,6 +25,8 @@
     <h3 class="sr-only">[% l('Saved Searches') %]</h3>
     [% INCLUDE "opac/parts/staff_saved_searches.tt2" %]
     [%-  END %]
+    <h3 class="sr-only">[% l('Search Results filters') %]</h3>
+    [% INCLUDE 'opac/parts/result/adv_filter.tt2' %]
     <h3 class="sr-only">[% l('Search Results facets') %]</h3>
     [% INCLUDE 'opac/parts/result/facets.tt2' %]
     <h3 class="sr-only">[% l('Search Results List') %]</h3>
diff --git a/Open-ILS/src/templates/opac/parts/searchbar.tt2 b/Open-ILS/src/templates/opac/parts/searchbar.tt2
index a48c4a0..2c162fe 100644
--- a/Open-ILS/src/templates/opac/parts/searchbar.tt2
+++ b/Open-ILS/src/templates/opac/parts/searchbar.tt2
@@ -17,7 +17,7 @@
             <label id="search_box_label" for="search_box">[% l('Search: ') %]
             <input type="text" id="search_box" name="query" aria-label="[%
                     l('Enter search query:');
-                %]" value="[% is_advanced ? ctx.naive_query_scrub(ctx.processed_search_query) : CGI.param('query') | html %]"
+                %]" value="[% is_advanced ? ctx.naive_query_scrub(ctx.user_query) : CGI.param('query') | html %]"
                 [%- IF use_autosuggest.enabled == "t" %]
                 dojoType="openils.widget.AutoSuggest" type_selector="'qtype'"
                 submitter="this.textbox.form.submit();"
@@ -85,6 +85,14 @@
         [% IF ctx.processed_search_query OR (NOT is_advanced AND NOT is_special) %]
         <input name='page' type='hidden' value="0" />
         [% END %]
+        [% IF is_advanced;
+            FOR p IN CGI.params.keys;
+                NEXT UNLESS p.match('^fi:');
+                FOR pv IN CGI.params.$p;
+                    %]<input type="hidden" name="[% p %]" value="[% pv %]" />[%
+                END;
+            END;
+        END %]
         [% IF is_special %]
             <input type="hidden" name="_special" value="1" /> [%
             number_of_expert_rows = CGI.param('tag').list.size;

commit 2b808b2e7b493a9483f238551ae3c38f5d7703ad
Author: Mike Rylander <mrylander at gmail.com>
Date:   Thu Aug 25 17:48:02 2016 -0400

    LP#1005040: implement business logic
    
    This patch gut most of the top level Search/Biblio.pm wrapper,
    inlines opensearch search params, uses the new dispach method,
    for OpenSRF subrequests, and return the abstract query when
    requested.
    
    It also adds CDBI classes for asset.copy_location_group which
    is needed for looking them up at search time.
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm
index 5c07790..f595c28 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm
@@ -829,141 +829,21 @@ __PACKAGE__->register_method(
 }
 
 sub multiclass_query {
+    # arghash only really supports limit/offset anymore
     my($self, $conn, $arghash, $query, $docache) = @_;
 
-    $logger->debug("initial search query => $query");
-    my $orig_query = $query;
-
-    $query =~ s/\+/ /go;
-    $query =~ s/^\s+//go;
-
-    # convert convenience classes (e.g. kw for keyword) to the full class name
-    # ensure that the convenience class isn't part of a word (e.g. 'playhouse')
-    $query =~ s/(^|\s)kw(:|\|)/$1keyword$2/go;
-    $query =~ s/(^|\s)ti(:|\|)/$1title$2/go;
-    $query =~ s/(^|\s)au(:|\|)/$1author$2/go;
-    $query =~ s/(^|\s)su(:|\|)/$1subject$2/go;
-    $query =~ s/(^|\s)se(:|\|)/$1series$2/go;
-    $query =~ s/(^|\s)name(:|\|)/$1author$2/og;
-
-    $logger->debug("cleansed query string => $query");
-    my $search = {};
-
-    my $simple_class_re  = qr/((?:\w+(?:\|\w+)?):[^:]+?)$/;
-    my $class_list_re    = qr/(?:keyword|title|author|subject|series)/;
-    my $modifier_list_re = qr/(?:site|dir|sort|lang|available|preflib)/;
-
-    my $tmp_value = '';
-    while ($query =~ s/$simple_class_re//so) {
-
-        my $qpart = $1;
-        my $where = index($qpart,':');
-        my $type  = substr($qpart, 0, $where++);
-        my $value = substr($qpart, $where);
-
-        if ($type !~ /^(?:$class_list_re|$modifier_list_re)/o) {
-            $tmp_value = "$qpart $tmp_value";
-            next;
-        }
-
-        if ($type =~ /$class_list_re/o ) {
-            $value .= $tmp_value;
-            $tmp_value = '';
-        }
-
-        next unless $type and $value;
-
-        $value =~ s/^\s*//og;
-        $value =~ s/\s*$//og;
-        $type = 'sort_dir' if $type eq 'dir';
-
-        if($type eq 'site') {
-            # 'site' is the org shortname.  when using this, we also want 
-            # to search at the requested org's depth
-            my $e = new_editor();
-            if(my $org = $e->search_actor_org_unit({shortname => $value})->[0]) {
-                $arghash->{org_unit} = $org->id if $org;
-                $arghash->{depth} = $e->retrieve_actor_org_unit_type($org->ou_type)->depth;
-            } else {
-                $logger->warn("'site:' query used on invalid org shortname: $value ... ignoring");
-            }
-        } elsif($type eq 'pref_ou') {
-            # 'pref_ou' is the preferred org shortname.
-            my $e = new_editor();
-            if(my $org = $e->search_actor_org_unit({shortname => $value})->[0]) {
-                $arghash->{pref_ou} = $org->id if $org;
-            } else {
-                $logger->warn("'pref_ou:' query used on invalid org shortname: $value ... ignoring");
-            }
-
-        } elsif($type eq 'available') {
-            # limit to available
-            $arghash->{available} = 1 unless $value eq 'false' or $value eq '0';
-
-        } elsif($type eq 'lang') {
-            # collect languages into an array of languages
-            $arghash->{language} = [] unless $arghash->{language};
-            push(@{$arghash->{language}}, $value);
-
-        } elsif($type =~ /^sort/o) {
-            # sort and sort_dir modifiers
-            $arghash->{$type} = $value;
-
-        } else {
-            # append the search term to the term under construction
-            $search->{$type} =  {} unless $search->{$type};
-            $search->{$type}->{term} =  
-                ($search->{$type}->{term}) ? $search->{$type}->{term} . " $value" : $value;
-        }
-    }
-
-    $query .= " $tmp_value";
-    $query =~ s/\s+/ /go;
-    $query =~ s/^\s+//go;
-    $query =~ s/\s+$//go;
-
-    my $type = $arghash->{default_class} || 'keyword';
-    $type = ($type eq '-') ? 'keyword' : $type;
-    $type = ($type !~ /^(title|author|keyword|subject|series)(?:\|\w+)?$/o) ? 'keyword' : $type;
-
-    if($query) {
-        # This is the front part of the string before any special tokens were
-        # parsed OR colon-separated strings that do not denote a class.
-        # Add this data to the default search class
-        $search->{$type} =  {} unless $search->{$type};
-        $search->{$type}->{term} =
-            ($search->{$type}->{term}) ? $search->{$type}->{term} . " $query" : $query;
+    if ($query) {
+        $query =~ s/\+/ /go;
+        $query =~ s/^\s+//go;
+        $query =~ s/\s+/ /go;
+        $arghash->{query} = $query
     }
-    my $real_search = $arghash->{searches} = { $type => { term => $orig_query } };
-
-    # capture the original limit because the search method alters the limit internally
-    my $ol = $arghash->{limit};
-
-    my $sclient = OpenSRF::Utils::SettingsClient->new;
-
-    (my $method = $self->api_name) =~ s/\.query//o;
 
-    $method =~ s/multiclass/multiclass.staged/
-        if $sclient->config_value(apps => 'open-ils.search',
-            app_settings => 'use_staged_search') =~ /true/i;
+    $logger->debug("initial search query => $query") if $query;
 
-    # XXX This stops the session locale from doing the right thing.
-    # XXX Revisit this and have it translate to a lang instead of a locale.
-    #$arghash->{preferred_language} = $U->get_org_locale($arghash->{org_unit})
-    #    unless $arghash->{preferred_language};
+    (my $method = $self->api_name) =~ s/\.query/.staged/o;
+    return $self->method_lookup($method)->dispatch($arghash, $docache);
 
-    $method = $self->method_lookup($method);
-    my ($data) = $method->run($arghash, $docache);
-
-    $arghash->{searches} = $search if (!$data->{complex_query});
-
-    $arghash->{limit} = $ol if $ol;
-    $data->{compiled_search} = $arghash;
-    $data->{query} = $orig_query;
-
-    $logger->info("compiled search is " . OpenSRF::Utils::JSON->perl2JSON($arghash));
-
-    return $data;
 }
 
 __PACKAGE__->register_method(
@@ -1249,6 +1129,7 @@ __PACKAGE__->register_method(
     signature => q/The .staff search includes hidden bibs, hidden items and bibs with no items.  Otherwise, @see open-ils.search.biblio.multiclass.staged/
 );
 
+my $estimation_strategy;
 sub staged_search {
     my($self, $conn, $search_hash, $docache) = @_;
 
@@ -1261,10 +1142,12 @@ sub staged_search {
     $method .= '.staff' if $self->api_name =~ /staff$/;
     $method .= '.atomic';
                 
-    return {count => 0} unless (
-        $search_hash and 
-        $search_hash->{searches} and 
-        scalar( keys %{$search_hash->{searches}} ));
+    if (!$search_hash{query}) {
+        return {count => 0} unless (
+            $search_hash and 
+            $search_hash->{searches} and 
+            scalar( keys %{$search_hash->{searches}} ));
+    }
 
     my $search_duration;
     my $user_offset = $search_hash->{offset} ||  0; # user-specified offset
@@ -1285,11 +1168,13 @@ sub staged_search {
     $search_hash->{core_limit}  = $superpage_size * $max_superpages;
 
     # Set the configured estimation strategy, defaults to 'inclusion'.
-    my $estimation_strategy = OpenSRF::Utils::SettingsClient
-        ->new
-        ->config_value(
-            apps => 'open-ils.search', app_settings => 'estimation_strategy'
-        ) || 'inclusion';
+    unless ($estimation_strategy) {
+        $estimation_strategy = OpenSRF::Utils::SettingsClient
+            ->new
+            ->config_value(
+                apps => 'open-ils.search', app_settings => 'estimation_strategy'
+            ) || 'inclusion';
+    }
     $search_hash->{estimation_strategy} = $estimation_strategy;
 
     # pull any existing results from the cache
@@ -1343,6 +1228,7 @@ sub staged_search {
             # retrieve the window of results from the database
             $logger->debug("staged search: fetching results from the database");
             $search_hash->{skip_check} = $page * $superpage_size;
+            $search_hash->{return_query} = $page == 0 ? 1 : 0;
             my $start = time;
             $results = $U->storagereq($method, %$search_hash);
             $search_duration = time - $start;
@@ -1384,6 +1270,12 @@ sub staged_search {
 
         my $current_count = scalar(@$all_results);
 
+        if ($page == 0) {
+            foreach (qw/ query_struct canonicalized_query /) {
+                $global_summary->{$_} = $summary->{$_};
+            }
+        }
+
         $est_hit_count = $summary->{estimated_hit_count} || $summary->{visible}
             if $page == 0;
 
@@ -1444,6 +1336,7 @@ sub staged_search {
 
     $conn->respond_complete(
         {
+            global_summary    => $global_summary,
             count             => $est_hit_count,
             core_limit        => $search_hash->{core_limit},
             superpage_size    => $search_hash->{check_limit},
@@ -1453,9 +1346,9 @@ sub staged_search {
         }
     );
 
-    cache_facets($facet_key, $new_ids, $IAmMetabib, $ignore_facet_classes) if $docache;
+    $logger->info("Completed canonicalized search is: $$global_summary{canonicalized_query}");
 
-    return undef;
+    return cache_facets($facet_key, $new_ids, $IAmMetabib, $ignore_facet_classes) if $docache;
 }
 
 sub tag_circulated_records {
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 7aec5f6..df5d03b 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
@@ -2792,7 +2792,7 @@ __PACKAGE__->register_method(
 sub abstract_query2str {
     my ($self, $conn, $query) = @_;
 
-    return QueryParser::Canonicalize::abstract_query2str_impl($query, 0);
+    return QueryParser::Canonicalize::abstract_query2str_impl($query, 0, $OpenILS::Application::Storage::QParser);
 }
 
 __PACKAGE__->register_method(
@@ -3134,6 +3134,11 @@ sub query_parser_fts {
         $$summary_row{complex_query} = $query->simple_plan ? 0 : 1;
     }
 
+    if ($args{return_query}) {
+        $$summary_row{query_struct} = $query->parse_tree->to_abstract_query();
+        $$summary_row{canonicalized_query} = QueryParser::Canonicalize::abstract_query2str_impl($$summary_row{query_struct}, 0, $parser);
+    }
+
     $client->respond( $summary_row );
 
     $log->debug("Search yielded ".scalar(@$recs)." checked, visible results with an approximate visible total of $estimate.",DEBUG);
@@ -3173,17 +3178,20 @@ sub query_parser_fts_wrapper {
 
     _initialize_parser($parser) unless $parser->initialization_complete;
 
-    if (! scalar( keys %{$args{searches}} )) {
+    $args{searches} ||= {};
+    if (!scalar(keys(%{$args{searches}})) && !$args{query}) {
         die "No search arguments were passed to ".$self->api_name;
     }
 
     $top_org ||= actor::org_unit->search( { parent_ou => undef } )->next;
 
-    $log->debug("Constructing QueryParser query from staged search hash ...", DEBUG);
-    my $base_query = '';
-    for my $sclass ( keys %{$args{searches}} ) {
-        $log->debug(" --> staged search key: $sclass --> term: $args{searches}{$sclass}{term}", DEBUG);
-        $base_query .= " $sclass: $args{searches}{$sclass}{term}";
+    my $base_query = $args{query} || '';
+    if (scalar(keys($args{searches}))) {
+        $log->debug("Constructing QueryParser query from staged search hash ...", DEBUG);
+        for my $sclass ( keys %{$args{searches}} ) {
+            $log->debug(" --> staged search key: $sclass --> term: $args{searches}{$sclass}{term}", DEBUG);
+            $base_query .= " $sclass: $args{searches}{$sclass}{term}";
+        }
     }
 
     my $query = $base_query;
@@ -3262,6 +3270,8 @@ sub query_parser_fts_wrapper {
 
     $query = "estimation_strategy($args{estimation_strategy}) $query" if ($args{estimation_strategy});
     $query = "badge_orgs($borgs) $query" if ($borgs);
+
+    # 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 = "depth($args{depth}) $query" if (defined($args{depth}));
     $query = "sort($args{sort}) $query" if ($args{sort});
@@ -3290,13 +3300,12 @@ sub query_parser_fts_wrapper {
         $args{item_form} = [ split '', $f ];
     }
 
-    for my $filter ( qw/locations location_groups statuses between audience language lit_form item_form item_type bib_level vr_format badges/ ) {
+    for my $filter ( qw/locations location_groups statuses audience language lit_form item_form item_type bib_level vr_format badges/ ) {
         if (my $s = $args{$filter}) {
             $s = [$s] if (!ref($s));
 
             my @filter_list = @$s;
 
-            next if ($filter eq 'between' and scalar(@filter_list) != 2);
             next if (@filter_list == 0);
 
             my $filter_string = join ',', @filter_list;
@@ -3306,7 +3315,7 @@ sub query_parser_fts_wrapper {
 
     $log->debug("Full QueryParser query: $query", DEBUG);
 
-    return query_parser_fts($self, $client, query => $query, _simple_plan => $base_plan->simple_plan );
+    return query_parser_fts($self, $client, query => $query, _simple_plan => $base_plan->simple_plan, return_query => $args{return_query} );
 }
 __PACKAGE__->register_method(
     api_name    => "open-ils.storage.biblio.multiclass.staged.search_fts",
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/QueryParser.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/QueryParser.pm
index 7f1f235..a4c74d1 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/QueryParser.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/QueryParser.pm
@@ -1459,10 +1459,9 @@ sub abstract_query2str_impl {
                 ($abstract_query->{content} || '') .
                 ($abstract_query->{suffix} || '');
         } elsif ($abstract_query->{type} eq 'facet') {
-            # facet syntax [ # ] is hardcoded I guess?
             my $prefix = $abstract_query->{negate} ? $qpconfig->{operators}{disallowed} : '';
             $q .= ($q ? ' ' : '') . $prefix . $abstract_query->{name} . "[" .
-                join(" # ", @{$abstract_query->{values}}) . "]";
+                join("][", @{$abstract_query->{values}}) . "]";
         }
     }
 
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/SuperCat.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/SuperCat.pm
index a338ebe..b4ba2d7 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/SuperCat.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/SuperCat.pm
@@ -1438,19 +1438,20 @@ sub opensearch_feed {
     my $org_unit = get_ou($org);
 
     # Apostrophes break search and get indexed as spaces anyway
+    # XXX ^that's kinda a lie ...
     my $safe_terms = $terms;
     $safe_terms =~ s{'}{ }go;
+    
+    my $query_terms = 'site('.$org_unit->[0]->shortname.") $safe_terms";
+    $query_tems = "sort($sort) $query_terms" if ($sort);
+    $query_tems = "language($lang) $query_terms" if ($lang);
+    $query_tems = "#$sortdir $query_terms" if ($sortdir);
 
     my $recs = $search->request(
         'open-ils.search.biblio.multiclass.query' => {
-            org_unit    => $org_unit->[0]->id,
             offset        => $offset,
-            limit        => $limit,
-            sort        => $sort,
-            sort_dir    => $sortdir,
-            default_class => $class,
-            ($lang ?    ( 'language' => $lang    ) : ()),
-        } => $safe_terms => 1
+            limit        => $limit
+        } => $query_terms => 1
     )->gather(1);
 
     $log->debug("Hits for [$terms]: $recs->{count}");

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

Summary of changes:
 .../lib/OpenILS/Application/Search/Biblio.pm       |  184 ++++-----------
 .../Application/Storage/Publisher/metabib.pm       |   29 ++-
 .../lib/OpenILS/Application/Storage/QueryParser.pm |  259 +++++++++++++++++++-
 .../perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm |   19 +-
 .../perlmods/lib/OpenILS/WWW/EGCatLoader/Util.pm   |   32 ++-
 Open-ILS/src/perlmods/lib/OpenILS/WWW/SuperCat.pm  |   20 +-
 .../src/perlmods/t/19-OpenILS-WWW-EGCatLoader.t    |   13 +-
 Open-ILS/src/sql/Pg/002.schema.config.sql          |    2 +-
 Open-ILS/src/sql/Pg/950.data.seed-values.sql       |   14 +-
 .../1007.data.give-crad-human-descriptions.sql     |   34 +++
 .../support-scripts/test-scripts/query_parser.pl   |   80 ++++---
 Open-ILS/src/templates/opac/css/style.css.tt2      |   35 +++-
 .../src/templates/opac/parts/result/adv_filter.tt2 |   92 +++++++
 .../src/templates/opac/parts/result/lowhits.tt2    |    4 +
 Open-ILS/src/templates/opac/parts/result/table.tt2 |   25 ++
 Open-ILS/src/templates/opac/parts/searchbar.tt2    |   35 +++-
 Open-ILS/web/css/skin/default/opac/semiauto.css    |    1 -
 .../OPAC/advanced_search_limiters.adoc             |   12 +
 18 files changed, 653 insertions(+), 237 deletions(-)
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/1007.data.give-crad-human-descriptions.sql
 create mode 100644 Open-ILS/src/templates/opac/parts/result/adv_filter.tt2
 create mode 100644 docs/RELEASE_NOTES_NEXT/OPAC/advanced_search_limiters.adoc


hooks/post-receive
-- 
Evergreen ILS




More information about the open-ils-commits mailing list