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

Evergreen Git git at git.evergreen-ils.org
Fri Jul 29 17:53:36 EDT 2016


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  d5ce5929495c31eeac6bfdc3df6bd3e275a3ed0d (commit)
       via  ac561d3b347e4f7393000a6b76ea1071927d9d4e (commit)
       via  940270761e1ef9c4ea51c8ad9229e3c9bd8d45a2 (commit)
       via  cf614499238ad958f34f91ec0107d482ac124d30 (commit)
       via  ae3bb32ede6e7569b935a92ed5bc7fc1cb0564cf (commit)
       via  3d5b37a1072bfd177bf484d02e717dae57a62427 (commit)
       via  df0d763642e5841baaac36d09b1fa9df525b2b67 (commit)
       via  36ed838b1bc73b4bb5957aa7a3da29fc94f2563f (commit)
       via  6fd72121637701f4dfc50e2cd3b2896c49197c5c (commit)
       via  4f48f276d052a6211be625d3b87d8077a789999c (commit)
       via  1fd8103bf33430fe0b4b5ae8d2b2b051802ac473 (commit)
       via  d4d2f42e8674f93284d2932e171bf9a4f9cc8cf4 (commit)
       via  701d069268f75620e145ea73a2e21aec7ec5db42 (commit)
       via  b6dd839f6c292849017cdc8a2f0ff590facd70cc (commit)
       via  8180b60d040d6b1025de21bbd587839076037493 (commit)
       via  1dd8d953b01ccedc5e42162d53471f132156e77a (commit)
      from  357aabae410bbe25160947acd52693841d690e92 (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 d5ce5929495c31eeac6bfdc3df6bd3e275a3ed0d
Author: Kathy Lussier <klussier at masslnc.org>
Date:   Fri Jul 29 17:51:14 2016 -0400

    LP#1549505: Stamping upgrade scripts 0983-84 for stat pop ratings
    
    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 2cc6ff5..0a00b31 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 ('0982', :eg_version); -- dyrcona/bshum
+INSERT INTO config.upgrade_log (version, applied_to) VALUES ('0984', :eg_version); -- miker/gmcharlt/kmlussier
 
 CREATE TABLE config.bib_source (
 	id		SERIAL	PRIMARY KEY,
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.statisitcal-ratings.sql b/Open-ILS/src/sql/Pg/upgrade/0983.schema.statistical-ratings.sql
similarity index 99%
rename from Open-ILS/src/sql/Pg/upgrade/XXXX.schema.statisitcal-ratings.sql
rename to Open-ILS/src/sql/Pg/upgrade/0983.schema.statistical-ratings.sql
index 29bcaf7..9c48c12 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.statisitcal-ratings.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/0983.schema.statistical-ratings.sql
@@ -1,6 +1,8 @@
 
 BEGIN;
 
+SELECT evergreen.upgrade_deps_block_check('0983', :eg_version);
+
 -- Create these so that the queries in the UDFs will validate
 CREATE TEMP TABLE precalc_filter_bib_list (
     id  BIGINT
diff --git a/Open-ILS/src/sql/Pg/upgrade/YYYY.function.qp_search.sql b/Open-ILS/src/sql/Pg/upgrade/0984.function.qp_search.sql
similarity index 99%
rename from Open-ILS/src/sql/Pg/upgrade/YYYY.function.qp_search.sql
rename to Open-ILS/src/sql/Pg/upgrade/0984.function.qp_search.sql
index 3cb1da6..ea58142 100644
--- a/Open-ILS/src/sql/Pg/upgrade/YYYY.function.qp_search.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/0984.function.qp_search.sql
@@ -17,6 +17,8 @@
 
 BEGIN;
 
+SELECT evergreen.upgrade_deps_block_check('0984', :eg_version);
+
 ALTER TYPE search.search_result ADD ATTRIBUTE badges TEXT, ADD ATTRIBUTE popularity NUMERIC;
 
 CREATE OR REPLACE FUNCTION search.query_parser_fts (

commit ac561d3b347e4f7393000a6b76ea1071927d9d4e
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Thu Jul 28 12:19:15 2016 -0400

    LP#1549505: update $modal to $uibModal
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/web/js/ui/default/staff/admin/local/rating/badge.js b/Open-ILS/web/js/ui/default/staff/admin/local/rating/badge.js
index 3df43e9..a4d9dcf 100644
--- a/Open-ILS/web/js/ui/default/staff/admin/local/rating/badge.js
+++ b/Open-ILS/web/js/ui/default/staff/admin/local/rating/badge.js
@@ -2,9 +2,9 @@ angular.module('egAdminRating',
     ['ngRoute','ui.bootstrap','egCoreMod','egUiMod','egGridMod'])
 
 .controller('Badges',
-       ['$scope','$q','$timeout','$location','$window','$modal',
+       ['$scope','$q','$timeout','$location','$window','$uibModal',
         'egCore','egGridDataProvider','egConfirmDialog',
-function($scope , $q , $timeout , $location , $window , $modal ,
+function($scope , $q , $timeout , $location , $window , $uibModal ,
          egCore , egGridDataProvider , egConfirmDialog) {
 
     egCore.startup.go();
@@ -95,10 +95,10 @@ function($scope , $q , $timeout , $location , $window , $modal ,
 
     function spawn_editor(rb, action) {
         var deferred = $q.defer();
-        $modal.open({
+        $uibModal.open({
             templateUrl: './admin/local/rating/edit_badge',
             controller:
-                ['$scope', '$modalInstance', function($scope, $modalInstance) {
+                ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
                 $scope.focusMe = true;
                 $scope.record = egCore.idl.toHash(rb);
                 // non-integer numeric field require parseFloat
@@ -106,8 +106,8 @@ function($scope , $q , $timeout , $location , $window , $modal ,
                 $scope.record_label = get_record_label();
                 $scope.fields = get_field_list($scope.record);
                 get_popularity_with_required();
-                $scope.ok = function(args) { $modalInstance.close(args) }
-                $scope.cancel = function () { $modalInstance.dismiss() }
+                $scope.ok = function(args) { $uibModalInstance.close(args) }
+                $scope.cancel = function () { $uibModalInstance.dismiss() }
             }]
         }).result.then(function(args) {
             var rb = new egCore.idl.rb();

commit 940270761e1ef9c4ea51c8ad9229e3c9bd8d45a2
Author: Kathy Lussier <klussier at masslnc.org>
Date:   Thu Jul 21 12:02:38 2016 -0400

    LP#1549505: Decrease value of Max popularity importance multiplier
    
    My testing found that a setting between 1.1 and 1.2 resulted in fairly good
    results when using the popularity-adjusted relevance sort. Adjusting the value
    in the seed data to 1.1 since it will be a better starting point for libraries.
    
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
index c66bf60..7cda842 100644
--- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql
+++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
@@ -11732,7 +11732,7 @@ INSERT INTO config.global_flag (name, label, value, enabled) VALUES (
         'cgf',
         'label'
     ),
-    '2.0',
+    '1.1',
     TRUE
 );
 
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.statisitcal-ratings.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.statisitcal-ratings.sql
index d6e5a22..29bcaf7 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.statisitcal-ratings.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.statisitcal-ratings.sql
@@ -143,7 +143,7 @@ INSERT INTO config.global_flag (name, label, value, enabled) VALUES (
         'cgf',
         'label'
     ),
-    '2.0',
+    '1.1',
     TRUE
 );
 

commit cf614499238ad958f34f91ec0107d482ac124d30
Author: Kathy Lussier <klussier at masslnc.org>
Date:   Thu Jul 21 11:52:24 2016 -0400

    LP#1549505: Remove stray semicolon from PgTap test
    
    The semicolon was causing failures in the test.
    
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/sql/Pg/t/regress/lp1549505_statistical_popularity_infrastructure.pg b/Open-ILS/src/sql/Pg/t/regress/lp1549505_statistical_popularity_infrastructure.pg
index e5d44b7..78149c2 100644
--- a/Open-ILS/src/sql/Pg/t/regress/lp1549505_statistical_popularity_infrastructure.pg
+++ b/Open-ILS/src/sql/Pg/t/regress/lp1549505_statistical_popularity_infrastructure.pg
@@ -26,7 +26,7 @@ SELECT is(
     score, 5,
     'LP#1549505: Badge caclulation framework is operational'
 )
-FROM rating.record_badge_score;
+FROM rating.record_badge_score
 WHERE record = 999999998
 AND badge = 999999998;
 

commit ae3bb32ede6e7569b935a92ed5bc7fc1cb0564cf
Author: Mike Rylander <mrylander at gmail.com>
Date:   Thu May 26 17:51:49 2016 -0400

    LP#1549505: Query literal interpolation casts incorrectly
    
    We need to be sure that all query values are of the same type in the same
    columns because PLPGSQL functions are compiled and cached.  In this case,
    the core query of the in-db search cannot have its shape change.  This
    commit assures that browse and search uses of the SP provide core queries
    that match on their SELECT lists.  Of particular importance is the type
    of the "rel" output column, which was variously float8 or numeric, depending
    on whether the search contained any terms (a "search") or not (a browse
    link).
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Driver/Pg/QueryParser.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Driver/Pg/QueryParser.pm
index 9265a88..4be8e93 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Driver/Pg/QueryParser.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Driver/Pg/QueryParser.pm
@@ -944,10 +944,10 @@ sub toSQL {
             # have an overage badge score of zero.
 
             my $adjusted_scale = ( $max_mult - 1.0 ) / 5.0;
-            $rank = "1.0/(( $rel ) * (1.0 + (AVG(COALESCE(pop_with.total_score::NUMERIC,0.0)) * $adjusted_scale)))::NUMERIC";
+            $rank = "1.0/(( $rel ) * (1.0 + (AVG(COALESCE(pop_with.total_score::NUMERIC,0.0::NUMERIC)) * ${adjusted_scale}::NUMERIC)))::NUMERIC";
         }
     } elsif ($sort_filter =~ /^pop/) {
-        $rank = '1.0/(AVG(COALESCE(pop_with.total_score::NUMERIC,0.0)) + 5.0)::NUMERIC';
+        $rank = '1.0/(AVG(COALESCE(pop_with.total_score::NUMERIC,0.0::NUMERIC)) + 5.0::NUMERIC)::NUMERIC';
         my $pop_desc = $desc eq 'ASC' ? 'DESC' : 'ASC';
         $pop_extra_sort = "3 $pop_desc $nullpos,";
     } else {
@@ -980,11 +980,11 @@ sub toSQL {
 $with
 SELECT  $key AS id,
         $agg_records,
-        $rel AS rel,
+        ${rel}::NUMERIC AS rel,
         $rank AS rank, 
         FIRST(pubdate_t.value) AS tie_break,
         STRING_AGG(ARRAY_TO_STRING(pop_with.badges,','),',') AS badges,
-        AVG(COALESCE(pop_with.total_score::NUMERIC,0.0))::NUMERIC(2,1) AS popularity
+        AVG(COALESCE(pop_with.total_score::NUMERIC,0.0::NUMERIC))::NUMERIC(2,1) AS popularity
   FROM  metabib.metarecord_source_map m
         $$flat_plan{from}
         $mra_join
diff --git a/Open-ILS/src/sql/Pg/030.schema.metabib.sql b/Open-ILS/src/sql/Pg/030.schema.metabib.sql
index 40f0dfb..0d3fb00 100644
--- a/Open-ILS/src/sql/Pg/030.schema.metabib.sql
+++ b/Open-ILS/src/sql/Pg/030.schema.metabib.sql
@@ -2192,8 +2192,8 @@ BEGIN
                 superpage_of_records := all_brecords[slice_start:slice_end];
                 qpfts_query :=
                     'SELECT NULL::BIGINT AS id, ARRAY[r] AS records, ' ||
-                    'NULL AS badges, NULL AS popularity, ' ||
-                    '1::INT AS rel FROM (SELECT UNNEST(' ||
+                    'NULL AS badges, NULL::NUMERIC AS popularity, ' ||
+                    '1::NUMERIC AS rel FROM (SELECT UNNEST(' ||
                     quote_literal(superpage_of_records) || '::BIGINT[]) AS r) rr';
 
                 -- We use search.query_parser_fts() for visibility testing.
@@ -2231,8 +2231,8 @@ BEGIN
                 superpage_of_records := all_arecords[slice_start:slice_end];
                 qpfts_query :=
                     'SELECT NULL::BIGINT AS id, ARRAY[r] AS records, ' ||
-                    'NULL AS badges, NULL AS popularity, ' ||
-                    '1::INT AS rel FROM (SELECT UNNEST(' ||
+                    'NULL AS badges, NULL::NUMERIC AS popularity, ' ||
+                    '1::NUMERIC AS rel FROM (SELECT UNNEST(' ||
                     quote_literal(superpage_of_records) || '::BIGINT[]) AS r) rr';
 
                 -- We use search.query_parser_fts() for visibility testing.
diff --git a/Open-ILS/src/sql/Pg/upgrade/YYYY.function.qp_search.sql b/Open-ILS/src/sql/Pg/upgrade/YYYY.function.qp_search.sql
index a4ea53d..3cb1da6 100644
--- a/Open-ILS/src/sql/Pg/upgrade/YYYY.function.qp_search.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/YYYY.function.qp_search.sql
@@ -484,8 +484,8 @@ BEGIN
                 superpage_of_records := all_brecords[slice_start:slice_end];
                 qpfts_query :=
                     'SELECT NULL::BIGINT AS id, ARRAY[r] AS records, ' ||
-                    'NULL AS badges, NULL AS popularity, ' ||
-                    '1::INT AS rel FROM (SELECT UNNEST(' ||
+                    'NULL AS badges, NULL::NUMERIC AS popularity, ' ||
+                    '1::NUMERIC AS rel FROM (SELECT UNNEST(' ||
                     quote_literal(superpage_of_records) || '::BIGINT[]) AS r) rr';
 
                 -- We use search.query_parser_fts() for visibility testing.
@@ -523,8 +523,8 @@ BEGIN
                 superpage_of_records := all_arecords[slice_start:slice_end];
                 qpfts_query :=
                     'SELECT NULL::BIGINT AS id, ARRAY[r] AS records, ' ||
-                    'NULL AS badges, NULL AS popularity, ' ||
-                    '1::INT AS rel FROM (SELECT UNNEST(' ||
+                    'NULL AS badges, NULL::NUMERIC AS popularity, ' ||
+                    '1::NUMERIC AS rel FROM (SELECT UNNEST(' ||
                     quote_literal(superpage_of_records) || '::BIGINT[]) AS r) rr';
 
                 -- We use search.query_parser_fts() for visibility testing.

commit 3d5b37a1072bfd177bf484d02e717dae57a62427
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Wed May 25 12:53:24 2016 -0400

    LP#1549505: fix staged browse
    
    This patch ensures that the core query passed to
    query_parser_fts by staged browse contains dummy
    badges and popularity columns (and thus, doesn't break);
    note that browse itself cares nothing about popularity.
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/sql/Pg/030.schema.metabib.sql b/Open-ILS/src/sql/Pg/030.schema.metabib.sql
index f7aa05a..40f0dfb 100644
--- a/Open-ILS/src/sql/Pg/030.schema.metabib.sql
+++ b/Open-ILS/src/sql/Pg/030.schema.metabib.sql
@@ -2192,6 +2192,7 @@ BEGIN
                 superpage_of_records := all_brecords[slice_start:slice_end];
                 qpfts_query :=
                     'SELECT NULL::BIGINT AS id, ARRAY[r] AS records, ' ||
+                    'NULL AS badges, NULL AS popularity, ' ||
                     '1::INT AS rel FROM (SELECT UNNEST(' ||
                     quote_literal(superpage_of_records) || '::BIGINT[]) AS r) rr';
 
@@ -2230,6 +2231,7 @@ BEGIN
                 superpage_of_records := all_arecords[slice_start:slice_end];
                 qpfts_query :=
                     'SELECT NULL::BIGINT AS id, ARRAY[r] AS records, ' ||
+                    'NULL AS badges, NULL AS popularity, ' ||
                     '1::INT AS rel FROM (SELECT UNNEST(' ||
                     quote_literal(superpage_of_records) || '::BIGINT[]) AS r) rr';
 
diff --git a/Open-ILS/src/sql/Pg/upgrade/YYYY.function.qp_search.sql b/Open-ILS/src/sql/Pg/upgrade/YYYY.function.qp_search.sql
index 9dbbfe5..a4ea53d 100644
--- a/Open-ILS/src/sql/Pg/upgrade/YYYY.function.qp_search.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/YYYY.function.qp_search.sql
@@ -392,5 +392,213 @@ BEGIN
 END;
 $func$ LANGUAGE PLPGSQL;
  
+CREATE OR REPLACE FUNCTION metabib.staged_browse(
+    query                   TEXT,
+    fields                  INT[],
+    context_org             INT,
+    context_locations       INT[],
+    staff                   BOOL,
+    browse_superpage_size   INT,
+    count_up_from_zero      BOOL,   -- if false, count down from -1
+    result_limit            INT,
+    next_pivot_pos          INT
+) RETURNS SETOF metabib.flat_browse_entry_appearance AS $p$
+DECLARE
+    curs                    REFCURSOR;
+    rec                     RECORD;
+    qpfts_query             TEXT;
+    aqpfts_query            TEXT;
+    afields                 INT[];
+    bfields                 INT[];
+    result_row              metabib.flat_browse_entry_appearance%ROWTYPE;
+    results_skipped         INT := 0;
+    row_counter             INT := 0;
+    row_number              INT;
+    slice_start             INT;
+    slice_end               INT;
+    full_end                INT;
+    all_records             BIGINT[];
+    all_brecords             BIGINT[];
+    all_arecords            BIGINT[];
+    superpage_of_records    BIGINT[];
+    superpage_size          INT;
+BEGIN
+    IF count_up_from_zero THEN
+        row_number := 0;
+    ELSE
+        row_number := -1;
+    END IF;
+
+    OPEN curs FOR EXECUTE query;
+
+    LOOP
+        FETCH curs INTO rec;
+        IF NOT FOUND THEN
+            IF result_row.pivot_point IS NOT NULL THEN
+                RETURN NEXT result_row;
+            END IF;
+            RETURN;
+        END IF;
+
+
+        -- Gather aggregate data based on the MBE row we're looking at now, authority axis
+        SELECT INTO all_arecords, result_row.sees, afields
+                ARRAY_AGG(DISTINCT abl.bib), -- bibs to check for visibility
+                STRING_AGG(DISTINCT aal.source::TEXT, $$,$$), -- authority record ids
+                ARRAY_AGG(DISTINCT map.metabib_field) -- authority-tag-linked CMF rows
+
+          FROM  metabib.browse_entry_simple_heading_map mbeshm
+                JOIN authority.simple_heading ash ON ( mbeshm.simple_heading = ash.id )
+                JOIN authority.authority_linking aal ON ( ash.record = aal.source )
+                JOIN authority.bib_linking abl ON ( aal.target = abl.authority )
+                JOIN authority.control_set_auth_field_metabib_field_map_refs map ON (
+                    ash.atag = map.authority_field
+                    AND map.metabib_field = ANY(fields)
+                )
+          WHERE mbeshm.entry = rec.id;
+
+
+        -- Gather aggregate data based on the MBE row we're looking at now, bib axis
+        SELECT INTO all_brecords, result_row.authorities, bfields
+                ARRAY_AGG(DISTINCT source),
+                STRING_AGG(DISTINCT authority::TEXT, $$,$$),
+                ARRAY_AGG(DISTINCT def)
+          FROM  metabib.browse_entry_def_map
+          WHERE entry = rec.id
+                AND def = ANY(fields);
+
+        SELECT INTO result_row.fields STRING_AGG(DISTINCT x::TEXT, $$,$$) FROM UNNEST(afields || bfields) x;
+
+        result_row.sources := 0;
+        result_row.asources := 0;
+
+        -- Bib-linked vis checking
+        IF ARRAY_UPPER(all_brecords,1) IS NOT NULL THEN
+
+            full_end := ARRAY_LENGTH(all_brecords, 1);
+            superpage_size := COALESCE(browse_superpage_size, full_end);
+            slice_start := 1;
+            slice_end := superpage_size;
+
+            WHILE result_row.sources = 0 AND slice_start <= full_end LOOP
+                superpage_of_records := all_brecords[slice_start:slice_end];
+                qpfts_query :=
+                    'SELECT NULL::BIGINT AS id, ARRAY[r] AS records, ' ||
+                    'NULL AS badges, NULL AS popularity, ' ||
+                    '1::INT AS rel FROM (SELECT UNNEST(' ||
+                    quote_literal(superpage_of_records) || '::BIGINT[]) AS r) rr';
+
+                -- We use search.query_parser_fts() for visibility testing.
+                -- We're calling it once per browse-superpage worth of records
+                -- out of the set of records related to a given mbe, until we've
+                -- either exhausted that set of records or found at least 1
+                -- visible record.
+
+                SELECT INTO result_row.sources visible
+                    FROM search.query_parser_fts(
+                        context_org, NULL, qpfts_query, NULL,
+                        context_locations, 0, NULL, NULL, FALSE, staff, FALSE
+                    ) qpfts
+                    WHERE qpfts.rel IS NULL;
+
+                slice_start := slice_start + superpage_size;
+                slice_end := slice_end + superpage_size;
+            END LOOP;
+
+            -- Accurate?  Well, probably.
+            result_row.accurate := browse_superpage_size IS NULL OR
+                browse_superpage_size >= full_end;
+
+        END IF;
+
+        -- Authority-linked vis checking
+        IF ARRAY_UPPER(all_arecords,1) IS NOT NULL THEN
+
+            full_end := ARRAY_LENGTH(all_arecords, 1);
+            superpage_size := COALESCE(browse_superpage_size, full_end);
+            slice_start := 1;
+            slice_end := superpage_size;
+
+            WHILE result_row.asources = 0 AND slice_start <= full_end LOOP
+                superpage_of_records := all_arecords[slice_start:slice_end];
+                qpfts_query :=
+                    'SELECT NULL::BIGINT AS id, ARRAY[r] AS records, ' ||
+                    'NULL AS badges, NULL AS popularity, ' ||
+                    '1::INT AS rel FROM (SELECT UNNEST(' ||
+                    quote_literal(superpage_of_records) || '::BIGINT[]) AS r) rr';
+
+                -- We use search.query_parser_fts() for visibility testing.
+                -- We're calling it once per browse-superpage worth of records
+                -- out of the set of records related to a given mbe, via
+                -- authority until we've either exhausted that set of records
+                -- or found at least 1 visible record.
+
+                SELECT INTO result_row.asources visible
+                    FROM search.query_parser_fts(
+                        context_org, NULL, qpfts_query, NULL,
+                        context_locations, 0, NULL, NULL, FALSE, staff, FALSE
+                    ) qpfts
+                    WHERE qpfts.rel IS NULL;
+
+                slice_start := slice_start + superpage_size;
+                slice_end := slice_end + superpage_size;
+            END LOOP;
+
+
+            -- Accurate?  Well, probably.
+            result_row.aaccurate := browse_superpage_size IS NULL OR
+                browse_superpage_size >= full_end;
+
+        END IF;
+
+        IF result_row.sources > 0 OR result_row.asources > 0 THEN
+
+            -- The function that calls this function needs row_number in order
+            -- to correctly order results from two different runs of this
+            -- functions.
+            result_row.row_number := row_number;
+
+            -- Now, if row_counter is still less than limit, return a row.  If
+            -- not, but it is less than next_pivot_pos, continue on without
+            -- returning actual result rows until we find
+            -- that next pivot, and return it.
+
+            IF row_counter < result_limit THEN
+                result_row.browse_entry := rec.id;
+                result_row.value := rec.value;
+
+                RETURN NEXT result_row;
+            ELSE
+                result_row.browse_entry := NULL;
+                result_row.authorities := NULL;
+                result_row.fields := NULL;
+                result_row.value := NULL;
+                result_row.sources := NULL;
+                result_row.sees := NULL;
+                result_row.accurate := NULL;
+                result_row.aaccurate := NULL;
+                result_row.pivot_point := rec.id;
+
+                IF row_counter >= next_pivot_pos THEN
+                    RETURN NEXT result_row;
+                    RETURN;
+                END IF;
+            END IF;
+
+            IF count_up_from_zero THEN
+                row_number := row_number + 1;
+            ELSE
+                row_number := row_number - 1;
+            END IF;
+
+            -- row_counter is different from row_number.
+            -- It simply counts up from zero so that we know when
+            -- we've reached our limit.
+            row_counter := row_counter + 1;
+        END IF;
+    END LOOP;
+END;
+$p$ LANGUAGE PLPGSQL;
+
 COMMIT;
 

commit df0d763642e5841baaac36d09b1fa9df525b2b67
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Fri May 6 11:29:39 2016 -0400

    LP#1549505: update baseline database schema
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/sql/Pg/090.schema.action.sql b/Open-ILS/src/sql/Pg/090.schema.action.sql
index c79793a..5ce80d3 100644
--- a/Open-ILS/src/sql/Pg/090.schema.action.sql
+++ b/Open-ILS/src/sql/Pg/090.schema.action.sql
@@ -432,6 +432,8 @@ CREATE INDEX hold_request_current_copy_before_cap_idx ON action.hold_request (cu
 CREATE UNIQUE INDEX hold_request_capture_protect_idx ON action.hold_request (current_copy) WHERE current_copy IS NOT NULL AND capture_time IS NOT NULL AND cancel_time IS NULL AND fulfillment_time IS NULL;
 CREATE INDEX hold_request_copy_capture_time_idx ON action.hold_request (current_copy,capture_time);
 CREATE INDEX hold_request_open_captured_shelf_lib_idx ON action.hold_request (current_shelf_lib) WHERE capture_time IS NOT NULL AND fulfillment_time IS NULL AND (pickup_lib <> current_shelf_lib);
+CREATE INDEX hold_fulfillment_time_idx ON action.hold_request (fulfillment_time) WHERE fulfillment_time IS NOT NULL;
+CREATE INDEX hold_request_time_idx ON action.hold_request (request_time);
 
 
 CREATE TABLE action.hold_request_note (
diff --git a/Open-ILS/src/sql/Pg/220.schema.rating.sql b/Open-ILS/src/sql/Pg/220.schema.rating.sql
new file mode 100644
index 0000000..460fc36
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/220.schema.rating.sql
@@ -0,0 +1,905 @@
+DROP SCHEMA IF EXISTS rating CASCADE;
+
+BEGIN;
+
+-- Create these so that the queries in the UDFs will validate
+CREATE TEMP TABLE precalc_filter_bib_list (
+    id  BIGINT
+) ON COMMIT DROP;
+
+CREATE TEMP TABLE precalc_bib_filter_bib_list (
+    id  BIGINT
+) ON COMMIT DROP;
+
+CREATE TEMP TABLE precalc_src_filter_bib_list (
+    id  BIGINT
+) ON COMMIT DROP;
+
+CREATE TEMP TABLE precalc_copy_filter_bib_list (
+    id  BIGINT,
+    copy  BIGINT
+) ON COMMIT DROP;
+
+CREATE TEMP TABLE precalc_circ_mod_filter_bib_list (
+    id  BIGINT,
+    copy  BIGINT
+) ON COMMIT DROP;
+
+CREATE TEMP TABLE precalc_location_filter_bib_list (
+    id  BIGINT,
+    copy  BIGINT
+) ON COMMIT DROP;
+
+CREATE TEMP TABLE precalc_attr_filter_bib_list (
+    id  BIGINT
+) ON COMMIT DROP;
+
+CREATE TEMP TABLE precalc_bibs_by_copy_list (
+    id  BIGINT
+) ON COMMIT DROP;
+
+CREATE TEMP TABLE precalc_bibs_by_uri_list (
+    id  BIGINT
+) ON COMMIT DROP;
+
+CREATE TEMP TABLE precalc_bibs_by_copy_or_uri_list (
+    id  BIGINT
+) ON COMMIT DROP;
+
+CREATE TEMP TABLE precalc_bib_list (
+    id  BIGINT
+) ON COMMIT DROP;
+
+CREATE SCHEMA rating;
+
+CREATE TABLE rating.popularity_parameter (
+    id          INT     PRIMARY KEY,
+    name        TEXT    NOT NULL UNIQUE, -- i18n
+    description TEXT,
+    func        TEXT,
+    require_horizon     BOOL    NOT NULL DEFAULT FALSE,
+    require_importance  BOOL    NOT NULL DEFAULT FALSE,
+    require_percentile  BOOL    NOT NULL DEFAULT FALSE
+);
+
+INSERT INTO rating.popularity_parameter (id,name,func,require_horizon,require_importance,require_percentile) VALUES
+    (1,'Holds Filled Over Time','rating.holds_filled_over_time',TRUE,FALSE,TRUE),
+    (2,'Holds Requested Over Time','rating.holds_placed_over_time',TRUE,FALSE,TRUE),
+    (3,'Current Hold Count','rating.current_hold_count',FALSE,FALSE,TRUE),
+    (4,'Circulations Over Time','rating.circs_over_time',TRUE,FALSE,TRUE),
+    (5,'Current Circulation Count','rating.current_circ_count',FALSE,FALSE,TRUE),
+    (6,'Out/Total Ratio','rating.checked_out_total_ratio',FALSE,FALSE,TRUE),
+    (7,'Holds/Total Ratio','rating.holds_total_ratio',FALSE,FALSE,TRUE),
+    (8,'Holds/Holdable Ratio','rating.holds_holdable_ratio',FALSE,FALSE,TRUE),
+    (9,'Percent of Time Circulating','rating.percent_time_circulating',FALSE,FALSE,TRUE),
+    (10,'Bibliographic Record Age (days, newer is better)','rating.bib_record_age',FALSE,FALSE,TRUE),
+    (11,'Publication Age (days, newer is better)','rating.bib_pub_age',FALSE,FALSE,TRUE),
+    (12,'On-line Bib has attributes','rating.generic_fixed_rating_by_uri',FALSE,FALSE,FALSE),
+    (13,'Bib has attributes and copies','rating.generic_fixed_rating_by_copy',FALSE,FALSE,FALSE),
+    (14,'Bib has attributes and copies or URIs','rating.generic_fixed_rating_by_copy_or_uri',FALSE,FALSE,FALSE),
+    (15,'Bib has attributes','rating.generic_fixed_rating_global',FALSE,FALSE,FALSE);
+
+CREATE TABLE rating.badge (
+    id                      SERIAL      PRIMARY KEY,
+    name                    TEXT        NOT NULL,
+    description             TEXT,
+    scope                   INT         NOT NULL REFERENCES actor.org_unit (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    weight                  INT         NOT NULL DEFAULT 1,
+    horizon_age             INTERVAL,
+    importance_age          INTERVAL,
+    importance_interval     INTERVAL    NOT NULL DEFAULT '1 day',
+    importance_scale        NUMERIC     CHECK (importance_scale IS NULL OR importance_scale > 0.0),
+    recalc_interval         INTERVAL    NOT NULL DEFAULT '1 month',
+    attr_filter             TEXT,
+    src_filter              INT         REFERENCES config.bib_source (id) ON UPDATE CASCADE ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
+    circ_mod_filter         TEXT        REFERENCES config.circ_modifier (code) ON UPDATE CASCADE ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
+    loc_grp_filter          INT         REFERENCES asset.copy_location_group (id) ON UPDATE CASCADE ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
+    popularity_parameter    INT         NOT NULL REFERENCES rating.popularity_parameter (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    fixed_rating            INT         CHECK (fixed_rating IS NULL OR fixed_rating BETWEEN -5 AND 5),
+    percentile              NUMERIC     CHECK (percentile IS NULL OR (percentile >= 50.0 AND percentile < 100.0)),
+    discard                 INT         NOT NULL DEFAULT 0, 
+    last_calc               TIMESTAMPTZ,
+    CONSTRAINT unique_name_scope UNIQUE (name,scope)
+);
+
+CREATE TABLE rating.record_badge_score (
+    id          BIGSERIAL   PRIMARY KEY,
+    record      BIGINT      NOT NULL REFERENCES biblio.record_entry (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    badge       INT         NOT NULL REFERENCES rating.badge (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    score       INT         NOT NULL CHECK (score BETWEEN -5 AND 5),
+    CONSTRAINT unique_record_badge UNIQUE (record,badge)
+);
+CREATE INDEX record_badge_score_badge_idx ON rating.record_badge_score (badge);
+CREATE INDEX record_badge_score_record_idx ON rating.record_badge_score (record);
+
+CREATE OR REPLACE VIEW rating.badge_with_orgs AS
+    WITH    org_scope AS (
+                SELECT  id,
+                        array_agg(tree) AS orgs
+                  FROM  (SELECT id,
+                                (actor.org_unit_descendants(id)).id AS tree
+                          FROM  actor.org_unit
+                        ) x
+                  GROUP BY 1
+            )
+    SELECT  b.*,
+            s.orgs
+      FROM  rating.badge b
+            JOIN org_scope s ON (b.scope = s.id);
+
+CREATE OR REPLACE FUNCTION rating.precalc_src_filter(src INT)
+    RETURNS INT AS $f$
+DECLARE
+    cnt     INT     := 0;
+BEGIN
+
+    SET LOCAL client_min_messages = error;
+    DROP TABLE IF EXISTS precalc_src_filter_bib_list;
+    IF src IS NOT NULL THEN
+        CREATE TEMP TABLE precalc_src_filter_bib_list ON COMMIT DROP AS
+            SELECT id FROM biblio.record_entry
+            WHERE source = src AND NOT deleted;
+    ELSE
+        CREATE TEMP TABLE precalc_src_filter_bib_list ON COMMIT DROP AS
+            SELECT id FROM biblio.record_entry
+            WHERE id > 0 AND NOT deleted;
+    END IF;
+
+    SELECT count(*) INTO cnt FROM precalc_src_filter_bib_list;
+    RETURN cnt;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION rating.precalc_circ_mod_filter(cm TEXT)
+    RETURNS INT AS $f$
+DECLARE
+    cnt     INT     := 0;
+BEGIN
+
+    SET LOCAL client_min_messages = error;
+    DROP TABLE IF EXISTS precalc_circ_mod_filter_bib_list;
+    IF cm IS NOT NULL THEN
+        CREATE TEMP TABLE precalc_circ_mod_filter_bib_list ON COMMIT DROP AS
+            SELECT  cn.record AS id,
+                    cp.id AS copy
+              FROM  asset.call_number cn
+                    JOIN asset.copy cp ON (cn.id = cp.call_number)
+              WHERE cp.circ_modifier = cm
+                    AND NOT cp.deleted;
+    ELSE
+        CREATE TEMP TABLE precalc_circ_mod_filter_bib_list ON COMMIT DROP AS
+            SELECT  cn.record AS id,
+                    cp.id AS copy
+              FROM  asset.call_number cn
+                    JOIN asset.copy cp ON (cn.id = cp.call_number)
+              WHERE NOT cp.deleted;
+    END IF;
+
+    SELECT count(*) INTO cnt FROM precalc_circ_mod_filter_bib_list;
+    RETURN cnt;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION rating.precalc_location_filter(loc INT)
+    RETURNS INT AS $f$
+DECLARE
+    cnt     INT     := 0;
+BEGIN
+
+    SET LOCAL client_min_messages = error;
+    DROP TABLE IF EXISTS precalc_location_filter_bib_list;
+    IF loc IS NOT NULL THEN
+        CREATE TEMP TABLE precalc_location_filter_bib_list ON COMMIT DROP AS
+            SELECT  cn.record AS id,
+                    cp.id AS copy
+              FROM  asset.call_number cn
+                    JOIN asset.copy cp ON (cn.id = cp.call_number)
+                    JOIN asset.copy_location_group_map lg ON (cp.location = lg.location)
+              WHERE lg.lgroup = loc
+                    AND NOT cp.deleted;
+    ELSE
+        CREATE TEMP TABLE precalc_location_filter_bib_list ON COMMIT DROP AS
+            SELECT  cn.record AS id,
+                    cp.id AS copy
+              FROM  asset.call_number cn
+                    JOIN asset.copy cp ON (cn.id = cp.call_number)
+              WHERE NOT cp.deleted;
+    END IF;
+
+    SELECT count(*) INTO cnt FROM precalc_location_filter_bib_list;
+    RETURN cnt;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+-- all or limited...
+CREATE OR REPLACE FUNCTION rating.precalc_attr_filter(attr_filter TEXT)
+    RETURNS INT AS $f$
+DECLARE
+    cnt     INT := 0;
+    afilter TEXT;
+BEGIN
+
+    SET LOCAL client_min_messages = error;
+    DROP TABLE IF EXISTS precalc_attr_filter_bib_list;
+    IF attr_filter IS NOT NULL THEN
+        afilter := metabib.compile_composite_attr(attr_filter);
+        CREATE TEMP TABLE precalc_attr_filter_bib_list ON COMMIT DROP AS
+            SELECT source AS id FROM metabib.record_attr_vector_list
+            WHERE vlist @@ metabib.compile_composite_attr(attr_filter);
+    ELSE
+        CREATE TEMP TABLE precalc_attr_filter_bib_list ON COMMIT DROP AS
+            SELECT source AS id FROM metabib.record_attr_vector_list;
+    END IF;
+
+    SELECT count(*) INTO cnt FROM precalc_attr_filter_bib_list;
+    RETURN cnt;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION rating.precalc_bibs_by_copy(badge_id INT)
+    RETURNS INT AS $f$
+DECLARE
+    cnt         INT     := 0;
+    badge_row   rating.badge_with_orgs%ROWTYPE;
+    base        TEXT;
+    whr         TEXT;
+BEGIN
+
+    SELECT * INTO badge_row FROM rating.badge_with_orgs WHERE id = badge_id;
+
+    SET LOCAL client_min_messages = error;
+    DROP TABLE IF EXISTS precalc_bibs_by_copy_list;
+    CREATE TEMP TABLE precalc_bibs_by_copy_list ON COMMIT DROP AS
+        SELECT  DISTINCT cn.record AS id
+          FROM  asset.call_number cn
+                JOIN asset.copy cp ON (cp.call_number = cn.id AND NOT cp.deleted)
+                JOIN precalc_copy_filter_bib_list f ON (cp.id = f.copy)
+          WHERE cn.owning_lib = ANY (badge_row.orgs)
+                AND NOT cn.deleted;
+
+    SELECT count(*) INTO cnt FROM precalc_bibs_by_copy_list;
+    RETURN cnt;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION rating.precalc_bibs_by_uri(badge_id INT)
+    RETURNS INT AS $f$
+DECLARE
+    cnt         INT     := 0;
+    badge_row   rating.badge_with_orgs%ROWTYPE;
+BEGIN
+
+    SELECT * INTO badge_row FROM rating.badge_with_orgs WHERE id = badge_id;
+
+    SET LOCAL client_min_messages = error;
+    DROP TABLE IF EXISTS precalc_bibs_by_uri_list;
+    CREATE TEMP TABLE precalc_bibs_by_uri_list ON COMMIT DROP AS
+        SELECT  DISTINCT record AS id
+          FROM  asset.call_number cn
+                JOIN asset.uri_call_number_map urim ON (urim.call_number = cn.id)
+                JOIN asset.uri uri ON (urim.uri = uri.id AND uri.active)
+          WHERE cn.owning_lib = ANY (badge_row.orgs)
+                AND cn.label = '##URI##'
+                AND NOT cn.deleted;
+
+    SELECT count(*) INTO cnt FROM precalc_bibs_by_uri_list;
+    RETURN cnt;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION rating.precalc_bibs_by_copy_or_uri(badge_id INT)
+    RETURNS INT AS $f$
+DECLARE
+    cnt         INT     := 0;
+BEGIN
+
+    PERFORM rating.precalc_bibs_by_copy(badge_id);
+    PERFORM rating.precalc_bibs_by_uri(badge_id);
+
+    SET LOCAL client_min_messages = error;
+    DROP TABLE IF EXISTS precalc_bibs_by_copy_or_uri_list;
+    CREATE TEMP TABLE precalc_bibs_by_copy_or_uri_list ON COMMIT DROP AS
+        SELECT id FROM precalc_bibs_by_copy_list
+            UNION
+        SELECT id FROM precalc_bibs_by_uri_list;
+
+    SELECT count(*) INTO cnt FROM precalc_bibs_by_copy_or_uri_list;
+    RETURN cnt;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+
+CREATE OR REPLACE FUNCTION rating.recalculate_badge_score ( badge_id INT, setup_only BOOL DEFAULT FALSE ) RETURNS VOID AS $f$
+DECLARE
+    badge_row           rating.badge%ROWTYPE;
+    param           rating.popularity_parameter%ROWTYPE;
+BEGIN
+    SET LOCAL client_min_messages = error;
+
+    -- Find what we're doing    
+    SELECT * INTO badge_row FROM rating.badge WHERE id = badge_id;
+    SELECT * INTO param FROM rating.popularity_parameter WHERE id = badge_row.popularity_parameter;
+
+    -- Calculate the filtered bib set, or all bibs if none
+    PERFORM rating.precalc_attr_filter(badge_row.attr_filter);
+    PERFORM rating.precalc_src_filter(badge_row.src_filter);
+    PERFORM rating.precalc_circ_mod_filter(badge_row.circ_mod_filter);
+    PERFORM rating.precalc_location_filter(badge_row.loc_grp_filter);
+
+    -- Bring the bib-level filter lists together
+    DROP TABLE IF EXISTS precalc_bib_filter_bib_list;
+    CREATE TEMP TABLE precalc_bib_filter_bib_list ON COMMIT DROP AS
+        SELECT id FROM precalc_attr_filter_bib_list
+            INTERSECT
+        SELECT id FROM precalc_src_filter_bib_list;
+
+    -- Bring the copy-level filter lists together. We're keeping this for bib_by_copy filtering later.
+    DROP TABLE IF EXISTS precalc_copy_filter_bib_list;
+    CREATE TEMP TABLE precalc_copy_filter_bib_list ON COMMIT DROP AS
+        SELECT id, copy FROM precalc_circ_mod_filter_bib_list
+            INTERSECT
+        SELECT id, copy FROM precalc_location_filter_bib_list;
+
+    -- Bring the collapsed filter lists together
+    DROP TABLE IF EXISTS precalc_filter_bib_list;
+    CREATE TEMP TABLE precalc_filter_bib_list ON COMMIT DROP AS
+        SELECT id FROM precalc_bib_filter_bib_list
+            INTERSECT
+        SELECT id FROM precalc_copy_filter_bib_list;
+
+    CREATE INDEX precalc_filter_bib_list_idx
+        ON precalc_filter_bib_list (id);
+
+    IF setup_only THEN
+        RETURN;
+    END IF;
+
+    -- If it's a fixed-rating badge, just do it ...
+    IF badge_row.fixed_rating IS NOT NULL THEN
+        DELETE FROM rating.record_badge_score WHERE badge = badge_id;
+        EXECUTE $e$
+            INSERT INTO rating.record_badge_score (record, badge, score)
+                SELECT record, $1, $2 FROM $e$ || param.func || $e$($1)$e$
+        USING badge_id, badge_row.fixed_rating;
+
+        UPDATE rating.badge SET last_calc = NOW() WHERE id = badge_id;
+
+        RETURN;
+    END IF;
+    -- else, calculate!
+
+    -- Make a session-local scratchpad for calculating scores
+    CREATE TEMP TABLE record_score_scratchpad (
+        bib     BIGINT,
+        value   NUMERIC
+    ) ON COMMIT DROP;
+
+    -- Gather raw values
+    EXECUTE $e$
+        INSERT INTO record_score_scratchpad (bib, value)
+            SELECT * FROM $e$ || param.func || $e$($1)$e$
+    USING badge_id;
+
+    IF badge_row.discard > 0 OR badge_row.percentile IS NOT NULL THEN
+        -- To speed up discard-common
+        CREATE INDEX record_score_scratchpad_score_idx ON record_score_scratchpad (value);
+        ANALYZE record_score_scratchpad;
+    END IF;
+
+    IF badge_row.discard > 0 THEN -- Remove common low values (trim the long tail)
+        DELETE FROM record_score_scratchpad WHERE value IN (
+            SELECT DISTINCT value FROM record_score_scratchpad ORDER BY value LIMIT badge_row.discard
+        );
+    END IF;
+
+    IF badge_row.percentile IS NOT NULL THEN -- Cut population down to exceptional records
+        DELETE FROM record_score_scratchpad WHERE value <= (
+            SELECT value FROM (
+                SELECT  value,
+                        CUME_DIST() OVER (ORDER BY value) AS p
+                  FROM  record_score_scratchpad
+            ) x WHERE p < badge_row.percentile / 100.0 ORDER BY p DESC LIMIT 1
+        );
+    END IF;
+
+
+    -- And, finally, push new data in
+    DELETE FROM rating.record_badge_score WHERE badge = badge_id;
+    INSERT INTO rating.record_badge_score (badge, record, score)
+        SELECT  badge_id,
+                bib,
+                GREATEST(ROUND((CUME_DIST() OVER (ORDER BY value)) * 5), 1) AS value
+          FROM  record_score_scratchpad;
+
+    DROP TABLE record_score_scratchpad;
+
+    -- Now, finally-finally, mark the badge as recalculated
+    UPDATE rating.badge SET last_calc = NOW() WHERE id = badge_id;
+
+    RETURN;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION rating.holds_filled_over_time(badge_id INT)
+    RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
+DECLARE
+    badge   rating.badge_with_orgs%ROWTYPE;
+    iage    INT     := 1;
+    iint    INT     := NULL;
+    iscale  NUMERIC := NULL;
+BEGIN
+
+    SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
+
+    IF badge.horizon_age IS NULL THEN
+        RAISE EXCEPTION 'Badge "%" with id % requires a horizon age but has none.',
+            badge.name,
+            badge.id;
+    END IF;
+
+    PERFORM rating.precalc_bibs_by_copy(badge_id);
+
+    SET LOCAL client_min_messages = error;
+    DROP TABLE IF EXISTS precalc_bib_list;
+    CREATE TEMP TABLE precalc_bib_list ON COMMIT DROP AS
+        SELECT id FROM precalc_filter_bib_list
+            INTERSECT
+        SELECT id FROM precalc_bibs_by_copy_list;
+
+    iint := EXTRACT(EPOCH FROM badge.importance_interval);
+    IF badge.importance_age IS NOT NULL THEN
+        iage := (EXTRACT(EPOCH FROM badge.importance_age) / iint)::INT;
+    END IF;
+
+    -- if iscale is smaller than 1, scaling slope will be shallow ... BEWARE!
+    iscale := COALESCE(badge.importance_scale, 1.0);
+
+    RETURN QUERY
+     SELECT bib,
+            SUM( holds * GREATEST( iscale * (iage - hage), 1.0 ))
+      FROM (
+         SELECT f.id AS bib,
+                (1 + EXTRACT(EPOCH FROM AGE(h.fulfillment_time)) / iint)::INT AS hage,
+                COUNT(h.id)::INT AS holds
+          FROM  action.hold_request h
+                JOIN reporter.hold_request_record rhrr ON (rhrr.id = h.id)
+                JOIN precalc_bib_list f ON (f.id = rhrr.bib_record)
+          WHERE h.fulfillment_time >= NOW() - badge.horizon_age
+                AND h.request_lib = ANY (badge.orgs)
+          GROUP BY 1, 2
+      ) x
+      GROUP BY 1;
+END;
+$f$ LANGUAGE PLPGSQL STRICT;
+
+CREATE OR REPLACE FUNCTION rating.holds_placed_over_time(badge_id INT)
+    RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
+DECLARE
+    badge   rating.badge_with_orgs%ROWTYPE;
+    iage    INT     := 1;
+    iint    INT     := NULL;
+    iscale  NUMERIC := NULL;
+BEGIN
+
+    SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
+
+    IF badge.horizon_age IS NULL THEN
+        RAISE EXCEPTION 'Badge "%" with id % requires a horizon age but has none.',
+            badge.name,
+            badge.id;
+    END IF;
+
+    PERFORM rating.precalc_bibs_by_copy(badge_id);
+
+    SET LOCAL client_min_messages = error;
+    DROP TABLE IF EXISTS precalc_bib_list;
+    CREATE TEMP TABLE precalc_bib_list ON COMMIT DROP AS
+        SELECT id FROM precalc_filter_bib_list
+            INTERSECT
+        SELECT id FROM precalc_bibs_by_copy_list;
+
+    iint := EXTRACT(EPOCH FROM badge.importance_interval);
+    IF badge.importance_age IS NOT NULL THEN
+        iage := (EXTRACT(EPOCH FROM badge.importance_age) / iint)::INT;
+    END IF;
+
+    -- if iscale is smaller than 1, scaling slope will be shallow ... BEWARE!
+    iscale := COALESCE(badge.importance_scale, 1.0);
+
+    RETURN QUERY
+     SELECT bib,
+            SUM( holds * GREATEST( iscale * (iage - hage), 1.0 ))
+      FROM (
+         SELECT f.id AS bib,
+                (1 + EXTRACT(EPOCH FROM AGE(h.request_time)) / iint)::INT AS hage,
+                COUNT(h.id)::INT AS holds
+          FROM  action.hold_request h
+                JOIN reporter.hold_request_record rhrr ON (rhrr.id = h.id)
+                JOIN precalc_bib_list f ON (f.id = rhrr.bib_record)
+          WHERE h.request_time >= NOW() - badge.horizon_age
+                AND h.request_lib = ANY (badge.orgs)
+          GROUP BY 1, 2
+      ) x
+      GROUP BY 1;
+END;
+$f$ LANGUAGE PLPGSQL STRICT;
+
+CREATE OR REPLACE FUNCTION rating.current_hold_count(badge_id INT)
+    RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
+DECLARE
+    badge   rating.badge_with_orgs%ROWTYPE;
+BEGIN
+
+    SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
+
+    PERFORM rating.precalc_bibs_by_copy(badge_id);
+
+    DELETE FROM precalc_copy_filter_bib_list WHERE id NOT IN (
+        SELECT id FROM precalc_filter_bib_list
+            INTERSECT
+        SELECT id FROM precalc_bibs_by_copy_list
+    );
+
+    ANALYZE precalc_copy_filter_bib_list;
+
+    RETURN QUERY
+     SELECT rhrr.bib_record AS bib,
+            COUNT(DISTINCT h.id)::NUMERIC AS holds
+      FROM  action.hold_request h
+            JOIN reporter.hold_request_record rhrr ON (rhrr.id = h.id)
+            JOIN action.hold_copy_map m ON (m.hold = h.id)
+            JOIN precalc_copy_filter_bib_list cf ON (rhrr.bib_record = cf.id AND m.target_copy = cf.copy)
+      WHERE h.fulfillment_time IS NULL
+            AND h.request_lib = ANY (badge.orgs)
+      GROUP BY 1;
+END;
+$f$ LANGUAGE PLPGSQL STRICT;
+
+CREATE OR REPLACE FUNCTION rating.circs_over_time(badge_id INT)
+    RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
+DECLARE
+    badge   rating.badge_with_orgs%ROWTYPE;
+    iage    INT     := 1;
+    iint    INT     := NULL;
+    iscale  NUMERIC := NULL;
+BEGIN
+
+    SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
+
+    IF badge.horizon_age IS NULL THEN
+        RAISE EXCEPTION 'Badge "%" with id % requires a horizon age but has none.',
+            badge.name,
+            badge.id;
+    END IF;
+
+    PERFORM rating.precalc_bibs_by_copy(badge_id);
+
+    DELETE FROM precalc_copy_filter_bib_list WHERE id NOT IN (
+        SELECT id FROM precalc_filter_bib_list
+            INTERSECT
+        SELECT id FROM precalc_bibs_by_copy_list
+    );
+
+    ANALYZE precalc_copy_filter_bib_list;
+
+    iint := EXTRACT(EPOCH FROM badge.importance_interval);
+    IF badge.importance_age IS NOT NULL THEN
+        iage := (EXTRACT(EPOCH FROM badge.importance_age) / iint)::INT;
+    END IF;
+
+    -- if iscale is smaller than 1, scaling slope will be shallow ... BEWARE!
+    iscale := COALESCE(badge.importance_scale, 1.0);
+
+    RETURN QUERY
+     SELECT bib,
+            SUM( circs * GREATEST( iscale * (iage - cage), 1.0 ))
+      FROM (
+         SELECT cn.record AS bib,
+                (1 + EXTRACT(EPOCH FROM AGE(c.xact_start)) / iint)::INT AS cage,
+                COUNT(c.id)::INT AS circs
+          FROM  action.circulation c
+                JOIN precalc_copy_filter_bib_list cf ON (c.target_copy = cf.copy)
+                JOIN asset.copy cp ON (cp.id = c.target_copy)
+                JOIN asset.call_number cn ON (cn.id = cp.call_number)
+          WHERE c.xact_start >= NOW() - badge.horizon_age
+                AND cn.owning_lib = ANY (badge.orgs)
+                AND c.phone_renewal IS FALSE  -- we don't count renewals
+                AND c.desk_renewal IS FALSE
+                AND c.opac_renewal IS FALSE
+          GROUP BY 1, 2
+      ) x
+      GROUP BY 1;
+END;
+$f$ LANGUAGE PLPGSQL STRICT;
+
+CREATE OR REPLACE FUNCTION rating.current_circ_count(badge_id INT)
+    RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
+DECLARE
+    badge   rating.badge_with_orgs%ROWTYPE;
+BEGIN
+
+    SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
+
+    PERFORM rating.precalc_bibs_by_copy(badge_id);
+
+    DELETE FROM precalc_copy_filter_bib_list WHERE id NOT IN (
+        SELECT id FROM precalc_filter_bib_list
+            INTERSECT
+        SELECT id FROM precalc_bibs_by_copy_list
+    );
+
+    ANALYZE precalc_copy_filter_bib_list;
+
+    RETURN QUERY
+     SELECT cn.record AS bib,
+            COUNT(c.id)::NUMERIC AS circs
+      FROM  action.circulation c
+            JOIN precalc_copy_filter_bib_list cf ON (c.target_copy = cf.copy)
+            JOIN asset.copy cp ON (cp.id = c.target_copy)
+            JOIN asset.call_number cn ON (cn.id = cp.call_number)
+      WHERE c.checkin_time IS NULL
+            AND cn.owning_lib = ANY (badge.orgs)
+      GROUP BY 1;
+
+END;
+$f$ LANGUAGE PLPGSQL STRICT;
+
+CREATE OR REPLACE FUNCTION rating.checked_out_total_ratio(badge_id INT)
+    RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
+DECLARE
+    badge   rating.badge_with_orgs%ROWTYPE;
+BEGIN
+
+    SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
+
+    PERFORM rating.precalc_bibs_by_copy(badge_id);
+
+    DELETE FROM precalc_copy_filter_bib_list WHERE id NOT IN (
+        SELECT id FROM precalc_filter_bib_list
+            INTERSECT
+        SELECT id FROM precalc_bibs_by_copy_list
+    );
+
+    ANALYZE precalc_copy_filter_bib_list;
+
+    RETURN QUERY
+     SELECT bib,
+            SUM(checked_out)::NUMERIC / SUM(total)::NUMERIC
+      FROM  (SELECT cn.record AS bib,
+                    (cp.status = 1)::INT AS checked_out,
+                    1 AS total
+              FROM  asset.copy cp
+                    JOIN precalc_copy_filter_bib_list c ON (cp.id = c.copy)
+                    JOIN asset.call_number cn ON (cn.id = cp.call_number)
+              WHERE cn.owning_lib = ANY (badge.orgs)
+            ) x
+      GROUP BY 1;
+END;
+$f$ LANGUAGE PLPGSQL STRICT;
+
+CREATE OR REPLACE FUNCTION rating.holds_total_ratio(badge_id INT)
+    RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
+DECLARE
+    badge   rating.badge_with_orgs%ROWTYPE;
+BEGIN
+
+    SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
+
+    PERFORM rating.precalc_bibs_by_copy(badge_id);
+
+    DELETE FROM precalc_copy_filter_bib_list WHERE id NOT IN (
+        SELECT id FROM precalc_filter_bib_list
+            INTERSECT
+        SELECT id FROM precalc_bibs_by_copy_list
+    );
+
+    ANALYZE precalc_copy_filter_bib_list;
+
+    RETURN QUERY
+     SELECT cn.record AS bib,
+            COUNT(DISTINCT m.hold)::NUMERIC / COUNT(DISTINCT cp.id)::NUMERIC
+      FROM  asset.copy cp
+            JOIN precalc_copy_filter_bib_list c ON (cp.id = c.copy)
+            JOIN asset.call_number cn ON (cn.id = cp.call_number)
+            JOIN action.hold_copy_map m ON (m.target_copy = cp.id)
+      WHERE cn.owning_lib = ANY (badge.orgs)
+      GROUP BY 1;
+END;
+$f$ LANGUAGE PLPGSQL STRICT;
+
+CREATE OR REPLACE FUNCTION rating.holds_holdable_ratio(badge_id INT)
+    RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
+DECLARE
+    badge   rating.badge_with_orgs%ROWTYPE;
+BEGIN
+
+    SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
+
+    PERFORM rating.precalc_bibs_by_copy(badge_id);
+
+    DELETE FROM precalc_copy_filter_bib_list WHERE id NOT IN (
+        SELECT id FROM precalc_filter_bib_list
+            INTERSECT
+        SELECT id FROM precalc_bibs_by_copy_list
+    );
+
+    ANALYZE precalc_copy_filter_bib_list;
+
+    RETURN QUERY
+     SELECT cn.record AS bib,
+            COUNT(DISTINCT m.hold)::NUMERIC / COUNT(DISTINCT cp.id)::NUMERIC
+      FROM  asset.copy cp
+            JOIN precalc_copy_filter_bib_list c ON (cp.id = c.copy)
+            JOIN asset.copy_location cl ON (cl.id = cp.location)
+            JOIN config.copy_status cs ON (cs.id = cp.status)
+            JOIN asset.call_number cn ON (cn.id = cp.call_number)
+            JOIN action.hold_copy_map m ON (m.target_copy = cp.id)
+      WHERE cn.owning_lib = ANY (badge.orgs)
+            AND cp.holdable IS TRUE
+            AND cl.holdable IS TRUE
+            AND cs.holdable IS TRUE
+      GROUP BY 1;
+END;
+$f$ LANGUAGE PLPGSQL STRICT;
+
+CREATE OR REPLACE FUNCTION rating.bib_record_age(badge_id INT)
+    RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
+DECLARE
+    badge   rating.badge_with_orgs%ROWTYPE;
+BEGIN
+
+    SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
+
+    PERFORM rating.precalc_bibs_by_copy_or_uri(badge_id);
+
+    SET LOCAL client_min_messages = error;
+    DROP TABLE IF EXISTS precalc_bib_list;
+    CREATE TEMP TABLE precalc_bib_list ON COMMIT DROP AS
+        SELECT id FROM precalc_filter_bib_list
+            INTERSECT
+        SELECT id FROM precalc_bibs_by_copy_or_uri_list;
+
+    RETURN QUERY
+     SELECT b.id,
+            1.0 / EXTRACT(EPOCH FROM AGE(b.create_date))::NUMERIC + 1.0
+      FROM  precalc_bib_list pop
+            JOIN biblio.record_entry b ON (b.id = pop.id);
+END;
+$f$ LANGUAGE PLPGSQL STRICT;
+
+CREATE OR REPLACE FUNCTION rating.bib_pub_age(badge_id INT)
+    RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
+DECLARE
+    badge   rating.badge_with_orgs%ROWTYPE;
+BEGIN
+
+    SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
+
+    PERFORM rating.precalc_bibs_by_copy_or_uri(badge_id);
+
+    SET LOCAL client_min_messages = error;
+    DROP TABLE IF EXISTS precalc_bib_list;
+    CREATE TEMP TABLE precalc_bib_list ON COMMIT DROP AS
+        SELECT id FROM precalc_filter_bib_list
+            INTERSECT
+        SELECT id FROM precalc_bibs_by_copy_or_uri_list;
+
+    RETURN QUERY
+     SELECT pop.id AS bib,
+            s.value::NUMERIC
+      FROM  precalc_bib_list pop
+            JOIN metabib.record_sorter s ON (
+                s.source = pop.id
+                AND s.attr = 'pubdate'
+                AND s.value ~ '^\d+$'
+            )
+      WHERE s.value::INT <= EXTRACT(YEAR FROM NOW())::INT;
+END;
+$f$ LANGUAGE PLPGSQL STRICT;
+
+CREATE OR REPLACE FUNCTION rating.percent_time_circulating(badge_id INT)
+    RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
+DECLARE
+    badge   rating.badge_with_orgs%ROWTYPE;
+BEGIN
+
+    SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
+
+    PERFORM rating.precalc_bibs_by_copy(badge_id);
+
+    DELETE FROM precalc_copy_filter_bib_list WHERE id NOT IN (
+        SELECT id FROM precalc_filter_bib_list
+            INTERSECT
+        SELECT id FROM precalc_bibs_by_copy_list
+    );
+
+    ANALYZE precalc_copy_filter_bib_list;
+
+    RETURN QUERY
+     SELECT bib,
+            SUM(COALESCE(circ_time,0))::NUMERIC / SUM(age)::NUMERIC
+      FROM  (SELECT cn.record AS bib,
+                    cp.id,
+                    EXTRACT( EPOCH FROM AGE(cp.active_date) ) + 1 AS age,
+                    SUM(  -- time copy spent circulating
+                        EXTRACT(
+                            EPOCH FROM
+                            AGE(
+                                COALESCE(circ.checkin_time, circ.stop_fines_time, NOW()),
+                                circ.xact_start
+                            )
+                        )
+                    )::NUMERIC AS circ_time
+              FROM  asset.copy cp
+                    JOIN precalc_copy_filter_bib_list c ON (cp.id = c.copy)
+                    JOIN asset.call_number cn ON (cn.id = cp.call_number)
+                    LEFT JOIN action.all_circulation circ ON (
+                        circ.target_copy = cp.id
+                        AND stop_fines NOT IN (
+                            'LOST',
+                            'LONGOVERDUE',
+                            'CLAIMSRETURNED',
+                            'LONGOVERDUE'
+                        )
+                        AND NOT (
+                            checkin_time IS NULL AND
+                            stop_fines = 'MAXFINES'
+                        )
+                    )
+              WHERE cn.owning_lib = ANY (badge.orgs)
+                    AND cp.active_date IS NOT NULL
+                    -- Next line requires that copies with no circs (circ.id IS NULL) also not be deleted
+                    AND ((circ.id IS NULL AND NOT cp.deleted) OR circ.id IS NOT NULL)
+              GROUP BY 1,2,3
+            ) x
+      GROUP BY 1;
+END;
+$f$ LANGUAGE PLPGSQL STRICT;
+
+CREATE OR REPLACE FUNCTION rating.generic_fixed_rating_by_copy(badge_id INT)
+    RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
+BEGIN
+    PERFORM rating.precalc_bibs_by_copy(badge_id);
+    RETURN QUERY
+        SELECT id, 1.0 FROM precalc_filter_bib_list
+            INTERSECT
+        SELECT id, 1.0 FROM precalc_bibs_by_copy_list;
+END;
+$f$ LANGUAGE PLPGSQL STRICT;
+
+CREATE OR REPLACE FUNCTION rating.generic_fixed_rating_by_uri(badge_id INT)
+    RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
+BEGIN
+    PERFORM rating.precalc_bibs_by_uri(badge_id);
+    RETURN QUERY
+        SELECT id, 1.0 FROM precalc_bib_filter_bib_list
+            INTERSECT
+        SELECT id, 1.0 FROM precalc_bibs_by_uri_list;
+END;
+$f$ LANGUAGE PLPGSQL STRICT;
+
+CREATE OR REPLACE FUNCTION rating.generic_fixed_rating_by_copy_or_uri(badge_id INT)
+    RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
+BEGIN
+    PERFORM rating.precalc_bibs_by_copy_or_uri(badge_id);
+    RETURN QUERY
+        (SELECT id, 1.0 FROM precalc_filter_bib_list
+            INTERSECT
+        SELECT id, 1.0 FROM precalc_bibs_by_copy_list)
+            UNION
+        (SELECT id, 1.0 FROM precalc_bib_filter_bib_list
+            INTERSECT
+        SELECT id, 1.0 FROM precalc_bibs_by_uri_list);
+END;
+$f$ LANGUAGE PLPGSQL STRICT;
+
+CREATE OR REPLACE FUNCTION rating.generic_fixed_rating_global(badge_id INT)
+    RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
+BEGIN
+    RETURN QUERY
+        SELECT id, 1.0 FROM precalc_bib_filter_bib_list;
+END;
+$f$ LANGUAGE PLPGSQL STRICT;
+
+COMMIT;
+
diff --git a/Open-ILS/src/sql/Pg/300.schema.staged_search.sql b/Open-ILS/src/sql/Pg/300.schema.staged_search.sql
index d5a5f0b..8aa18be 100644
--- a/Open-ILS/src/sql/Pg/300.schema.staged_search.sql
+++ b/Open-ILS/src/sql/Pg/300.schema.staged_search.sql
@@ -61,7 +61,7 @@ CREATE TABLE search.relevance_adjustment (
 );
 CREATE UNIQUE INDEX bump_once_per_field_idx ON search.relevance_adjustment ( field, bump_type );
 
-CREATE TYPE search.search_result AS ( id BIGINT, rel NUMERIC, record INT, total INT, checked INT, visible INT, deleted INT, excluded INT );
+CREATE TYPE search.search_result AS ( id BIGINT, rel NUMERIC, record INT, total INT, checked INT, visible INT, deleted INT, excluded INT, badges TEXT, popularity NUMERIC );
 CREATE TYPE search.search_args AS ( id INT, field_class TEXT, field_name TEXT, table_alias TEXT, term TEXT, term_type TEXT );
 
 CREATE OR REPLACE FUNCTION search.query_parser_fts (
@@ -190,6 +190,8 @@ BEGIN
 
                 current_res.id = core_result.id;
                 current_res.rel = core_result.rel;
+                current_res.badges = core_result.badges;
+                current_res.popularity = core_result.popularity;
 
                 tmp_int := 1;
                 IF metarecord THEN
@@ -225,6 +227,8 @@ BEGIN
 
                 current_res.id = core_result.id;
                 current_res.rel = core_result.rel;
+                current_res.badges = core_result.badges;
+                current_res.popularity = core_result.popularity;
 
                 tmp_int := 1;
                 IF metarecord THEN
@@ -393,6 +397,8 @@ BEGIN
 
         current_res.id = core_result.id;
         current_res.rel = core_result.rel;
+        current_res.badges = core_result.badges;
+        current_res.popularity = core_result.popularity;
 
         tmp_int := 1;
         IF metarecord THEN
@@ -416,6 +422,8 @@ BEGIN
     current_res.id = NULL;
     current_res.rel = NULL;
     current_res.record = NULL;
+    current_res.badges = NULL;
+    current_res.popularity = NULL;
     current_res.total = total_count;
     current_res.checked = check_count;
     current_res.deleted = deleted_count;
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 feb6cb4..c66bf60 100644
--- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql
+++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
@@ -11712,7 +11712,6 @@ INSERT INTO config.global_flag (name, label, value, enabled) VALUES (
     TRUE
 );
 
-/* To be added when upgrade scripts are pulled into the baseline
 INSERT INTO config.global_flag (name, label, value, enabled) VALUES (
     'opac.default_sort',
     oils_i18n_gettext(
@@ -11736,7 +11735,6 @@ INSERT INTO config.global_flag (name, label, value, enabled) VALUES (
     '2.0',
     TRUE
 );
-*/
 
 INSERT INTO config.usr_setting_type (name,opac_visible,label,description,datatype)
     VALUES (
diff --git a/Open-ILS/src/sql/Pg/reporter-schema.sql b/Open-ILS/src/sql/Pg/reporter-schema.sql
index 2e910f9..dce3603 100644
--- a/Open-ILS/src/sql/Pg/reporter-schema.sql
+++ b/Open-ILS/src/sql/Pg/reporter-schema.sql
@@ -257,25 +257,79 @@ SELECT	id,
 	END AS "type"
   FROM	action.circulation;
 
-CREATE OR REPLACE VIEW reporter.hold_request_record AS
-SELECT	id,
-	target,
-	hold_type,
-	CASE
-		WHEN hold_type = 'T'
-			THEN target
-		WHEN hold_type = 'I'
-			THEN (SELECT ssub.record_entry FROM serial.subscription ssub JOIN serial.issuance si ON (si.subscription = ssub.id) WHERE si.id = ahr.target)
-		WHEN hold_type = 'V'
-			THEN (SELECT cn.record FROM asset.call_number cn WHERE cn.id = ahr.target)
-		WHEN hold_type IN ('C','R','F')
-			THEN (SELECT cn.record FROM asset.call_number cn JOIN asset.copy cp ON (cn.id = cp.call_number) WHERE cp.id = ahr.target)
-		WHEN hold_type = 'M'
-			THEN (SELECT mr.master_record FROM metabib.metarecord mr WHERE mr.id = ahr.target)
-        WHEN hold_type = 'P'
-            THEN (SELECT bmp.record FROM biblio.monograph_part bmp WHERE bmp.id = ahr.target)
-	END AS bib_record
-  FROM	action.hold_request ahr;
+-- rhrr needs to be a real table, so it can be fast. To that end, we use
+-- a materialized view updated via a trigger.
+CREATE TABLE reporter.hold_request_record  AS
+SELECT  id,
+        target,
+        hold_type,
+        CASE
+                WHEN hold_type = 'T'
+                        THEN target
+                WHEN hold_type = 'I'
+                        THEN (SELECT ssub.record_entry FROM serial.subscription ssub JOIN serial.issuance si ON (si.subscription = ssub.id) WHERE si.id = ahr.target)
+                WHEN hold_type = 'V'
+                        THEN (SELECT cn.record FROM asset.call_number cn WHERE cn.id = ahr.target)
+                WHEN hold_type IN ('C','R','F')
+                        THEN (SELECT cn.record FROM asset.call_number cn JOIN asset.copy cp ON (cn.id = cp.call_number) WHERE cp.id = ahr.target)
+                WHEN hold_type = 'M'
+                        THEN (SELECT mr.master_record FROM metabib.metarecord mr WHERE mr.id = ahr.target)
+                WHEN hold_type = 'P'
+                        THEN (SELECT bmp.record FROM biblio.monograph_part bmp WHERE bmp.id = ahr.target)
+        END AS bib_record
+  FROM  action.hold_request ahr;
+
+CREATE UNIQUE INDEX reporter_hold_request_record_pkey_idx ON reporter.hold_request_record (id);
+CREATE INDEX reporter_hold_request_record_bib_record_idx ON reporter.hold_request_record (bib_record);
+
+ALTER TABLE reporter.hold_request_record ADD PRIMARY KEY USING INDEX reporter_hold_request_record_pkey_idx;
+
+CREATE FUNCTION reporter.hold_request_record_mapper () RETURNS TRIGGER AS $$
+BEGIN
+    IF TG_OP = 'INSERT' THEN
+        INSERT INTO reporter.hold_request_record (id, target, hold_type, bib_record)
+        SELECT  NEW.id,
+                NEW.target,
+                NEW.hold_type,
+                CASE
+                    WHEN NEW.hold_type = 'T'
+                        THEN NEW.target
+                    WHEN NEW.hold_type = 'I'
+                        THEN (SELECT ssub.record_entry FROM serial.subscription ssub JOIN serial.issuance si ON (si.subscription = ssub.id) WHERE si.id = NEW.target)
+                    WHEN NEW.hold_type = 'V'
+                        THEN (SELECT cn.record FROM asset.call_number cn WHERE cn.id = NEW.target)
+                    WHEN NEW.hold_type IN ('C','R','F')
+                        THEN (SELECT cn.record FROM asset.call_number cn JOIN asset.copy cp ON (cn.id = cp.call_number) WHERE cp.id = NEW.target)
+                    WHEN NEW.hold_type = 'M'
+                        THEN (SELECT mr.master_record FROM metabib.metarecord mr WHERE mr.id = NEW.target)
+                    WHEN NEW.hold_type = 'P'
+                        THEN (SELECT bmp.record FROM biblio.monograph_part bmp WHERE bmp.id = NEW.target)
+                END AS bib_record;
+    ELSIF TG_OP = 'UPDATE' AND (OLD.target <> NEW.target OR OLD.hold_type <> NEW.hold_type) THEN
+        UPDATE  reporter.hold_request_record
+          SET   target = NEW.target,
+                hold_type = NEW.hold_type,
+                bib_record = CASE
+                    WHEN NEW.hold_type = 'T'
+                        THEN NEW.target
+                    WHEN NEW.hold_type = 'I'
+                        THEN (SELECT ssub.record_entry FROM serial.subscription ssub JOIN serial.issuance si ON (si.subscription = ssub.id) WHERE si.id = NEW.target)
+                    WHEN NEW.hold_type = 'V'
+                        THEN (SELECT cn.record FROM asset.call_number cn WHERE cn.id = NEW.target)
+                    WHEN NEW.hold_type IN ('C','R','F')
+                        THEN (SELECT cn.record FROM asset.call_number cn JOIN asset.copy cp ON (cn.id = cp.call_number) WHERE cp.id = NEW.target)
+                    WHEN NEW.hold_type = 'M'
+                        THEN (SELECT mr.master_record FROM metabib.metarecord mr WHERE mr.id = NEW.target)
+                    WHEN NEW.hold_type = 'P'
+                        THEN (SELECT bmp.record FROM biblio.monograph_part bmp WHERE bmp.id = NEW.target)
+                END;
+    END IF;
+    RETURN NEW;
+END;
+$$ LANGUAGE PLPGSQL;
+
+CREATE TRIGGER reporter_hold_request_record_trigger AFTER INSERT OR UPDATE ON action.hold_request
+    FOR EACH ROW EXECUTE PROCEDURE reporter.hold_request_record_mapper();
 
 CREATE OR REPLACE VIEW reporter.xact_billing_totals AS
 SELECT	b.xact,
diff --git a/Open-ILS/src/sql/Pg/sql_file_manifest b/Open-ILS/src/sql/Pg/sql_file_manifest
index 639736b..9b16ab1 100644
--- a/Open-ILS/src/sql/Pg/sql_file_manifest
+++ b/Open-ILS/src/sql/Pg/sql_file_manifest
@@ -39,6 +39,8 @@ FTS_CONFIG_FILE
 200.schema.acq.sql
 201.acq.audit-functions.sql
 
+220.schema.rating.sql
+
 300.schema.staged_search.sql
 400.schema.action_trigger.sql
 

commit 36ed838b1bc73b4bb5957aa7a3da29fc94f2563f
Author: Jason Stephenson <jstephenson at mvlcstaff.org>
Date:   Fri Apr 15 09:36:51 2016 -0400

    LP 1549505: Fix syntax error in OpenILS/WWW/EGCatLoader/Record.pm
    
    Typo/error apparently introduced on line 72 in commit eeee27c.
    
    Signed-off-by: Jason Stephenson <jstephenson at mvlcstaff.org>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Record.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Record.pm
index 0253478..04f3cbe 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Record.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Record.pm
@@ -69,7 +69,7 @@ sub load_record {
     if ($self->cgi->param('badges')) {
         my $badges = $self->cgi->param('badges');
         $badges = $badges ? [split(',', $badges)] : [];
-        $badges = [grep { /^\d+$/ }, @$badges];
+        $badges = [grep { /^\d+$/ } @$badges];
         if (@$badges) {
             $self->ctx->{badge_scores} = $cstore->request(
                 'open-ils.cstore.direct.rating.record_badge_score.search.atomic',

commit 6fd72121637701f4dfc50e2cd3b2896c49197c5c
Author: Mike Rylander <mrylander at gmail.com>
Date:   Tue Apr 5 16:26:41 2016 -0400

    LP#1549505: Release notes for statistically generated record ratings (popularity)
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/docs/RELEASE_NOTES_NEXT/OPAC/popularity-rating.txt b/docs/RELEASE_NOTES_NEXT/OPAC/popularity-rating.txt
new file mode 100644
index 0000000..6383fd5
--- /dev/null
+++ b/docs/RELEASE_NOTES_NEXT/OPAC/popularity-rating.txt
@@ -0,0 +1,98 @@
+Statistically generated Record Ratings (Popularity)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Summary 
++++++++
+
+For the purpose of supplying non-bibliographic popularity adjustment to the ranking of search results, this feature implements a set of statistical modelling algorithms which will identify bibliographic records of particular note based on derivable parameters.
+
+Generally, factors such as to circulation and hold activity, record and item age, and item ownership counts will available for statistical consideration. Each factor will embody a "popularity badge" that the bibliographic record can earn, and each badge will have a 5-point scale, where more points indicates a more popular record.  The average of the badge points earned by each record will constitute a "popularity rating". The number and types of badges will break ties for average popularity, and relevance will sort items with like popularity. 
+
+A new sort axis of *Popularity* is created to sort first on the weighted average popularity of each record, followed by the query-specific relevance available today.  A new option is created in the dropdown that sorts on the combination of "activity metric" (aka badge ranking, aka popularity) first and the existing, stock relevance ranking when those are equal.  For instance, given two records that both have a badge ranking of "4.5", they will be sorted in the order of the query relevance ranking that is calculated today as a tie breaker.  Those two records will sort above other records with lower badge rankings regardless of what today's relevance ranking says about them.
+
+In addition, a new sort axis of *Popularity and Relevance* is created that augments the normal Relevance sort with a normalized popularity value by multiplying the base relevance by a value controlled by a new global flag, generally set to a decimal number between 1 and 2.
+
+Finally, there will continue to be a pure *Relevance* sort option, which is the version that exists today.
+
+A global flag will allow the selection of the default sort axis.
+
+
+The basics
+++++++++++
+
+There will exist two classes of non-bibliographic popularity badge: point-in-time popularity, such as the number of items held or the number of open hold requests; and temporal popularity, such as circulations over the past two years or hold requests placed over the last six months.
+
+Each popularity badge will have a definition.  The badge's value are calculated for each bibliographic record within the definition's bibliographic population.  The population circumscribes the bibliographic records that are eligible to receive the badge.  Each record within the population of a badge definition will receive a ranking adjustment, based on its "popularity rating" if the appropriate thresholds are met.
+
+The set of existing popularity badges is displayed as a grid.  A library selector defaulting to the workstation location will allow scoping the grid contents to specific organizational units.  Creating or editing a badge is performed within a dedicated modal popup interface that will float above the grid, disabling access to the underlying interface until the action is completed or canceled.
+
+All popularity badge definitions will describe a set of configuration, population criteria, and statistical constraints:
+
+* *Badge Name:* The administrator-assigned name of the popularity badge definition.  This is presented as a text input box, and the value is used in the OPAC.
+* *Scope:* The owning org unit of the badge.  Badges are cumulative, and are included in ranking when the Scope is equal to, or an ancestor of, the search location.  Therefore branch-specific searches will include branch, system and consortium badges, but consortium-wide searches will only make use of consortium-scoped badges.  For item-specific metrics, this also limits the population of the statistical group to those records having items whose owning library is at or below the Scope in the org tree.  This is presented as a standard Library selector.
+* *Weight:* A multiplier defining the importance of this particular badge in relation to any other badges that might be earned by a record.  This is presented as a number spinner with a default and minimum value of 1.
+* *Recalculation Interval:* How often to recalculate the badge's popularity value for each record.  For temporal popularity badges that may change quickly, such as in-house use or short-duration circulation-over-time, this may be nightly.  For slowly changing metrics such as count of items held, this may be monthly or quarterly. This is presented as a text input that will accept an interval string such as "2 years", "30 days", or "6 weeks, 2 days".  Numeric values without a timespan qualifier are considered to be a number of seconds.  For newer items that may have rapidly changing metrics, a mechanism is created to adjust the "last calculated date" so that library staff can clear the date and force a recalculation overnight. However, because the badge value each record receives is relative to all the other records in the population, the badge as a whole will need to be recalculated. This feature stores individual record raw stats, where possible and reasonable, to speed up r
 ecalculation.
+* *Population Filters:* Optional, and any combination may be used.
+** *Attribute Filter:* Filter bibliographic record population based on record attributes.  This will use an interface similar to the Composite Attribute editor.
+** *Bibliographic Source:* Filter bibliographic records to those with a specific source.  This is presented as a dropdown of sources.
+** *Circulation Modifier Filter:* Include only those items that make use of certain Circulation Modifiers in statistical calculations. This is only applicable to item-related badges.  This is presented as a dropdown of modifiers.
+** *Copy Location Group:* Include only those items that are assigned to shelving locations within specific location groups. This is only applicable to item-related badges.  This is presented as a dropdown of location groups available within the Scope for the badge.
+* *Popularity Parameter:* One of a set of predefined types of popularity measure.  This is presented as a dropdown.  These will include, but may not be limited to:
+** Holds Filled Over Time
+** Holds Requested Over Time
+** Current Hold Count
+** Circulations Over Time
+** Current Circulation Count
+** Out/Total Ratio
+** Holds/Total Ratio
+** Holds/Holdable Ratio
+** Percent of Time Circulating -- Of the time between the active date of the copies on the record and the badge calculation time, the percentage of that time during which the items have been checked out.  This is meant to be an indicator of high-demand over the lifetime of the title, and not just a temporary spike in circ count for, say, a book club or school report.  Recent temporary spikes can be represented by circs over time with a time horizon.  It's the difference between an "always popular" title and a "just recently popular" title.
+** Bibliographic Record Age (days)
+** Publication Age (days)
+** Available On-Line (for e-books, etc)
+* *Fixed Rating:* An optional override supplying a fixed popularity value for all records earning this badge.  For some popularity types, such as "Available On-Line", it is preferable to specify, rather than calculate, the popularity value for a badge.  This is presented as a number spinner with a value ranging from 1 to 5.
+* *Inclusion Threshold Percentile:* Cumulative distribution percentile.  This is presented as a number spinner of "percentile" values ranging from 50 to 100, indicating the number of how much of the population a record's score must be better than in order to earn the badge.  Leaving this value unset will allow the entire population to earn the badge.
+* *Discard most common:* A value that, if greater than 0, will ignore records with extremely common values so that outliers are more readily identified, and the distribution of values can be assumed to be more normal.  Many popularity parameters, such as those for circulation counts, benefit from this input filter.  This is presented as a number spinner initially set to 0 that indicates the number of distinct, low values -- but not the values themselves -- to exclude from the population.
+
+A word about Inclusion Threshold Percentile
++++++++++++++++++++++++++++++++++++++++++++
+
+In order to limit the amount of data that must be retained and queried during normal search operations, to allow limiting the popular population to truly exceptional records when appropriate, and to limit the speed cost of popularity ranking, many popularity types will provide thresholds that must be passed in order to store a record's badge-specific popularity value.
+
+The administrator will be presented with a choice of "percentile" that defines the threshold which must be crossed before the value of the variable for any given record is considered statistically significant, and therefore scaled and included in ranking.  For instance, a record may need to be in the 95th percentile for Holds/Total Items before it is considered "popular" and the badge is earned.
+
+Additionally, in order to normalize populations that exhibit a "long tail" effect, such as for circulation counts were most records will have a very low number of events, the administrator will be able to instruct the algorithm to ignore the most common low values.
+
+Type-specific input modifications
++++++++++++++++++++++++++++++++++
+
+For temporal popularity badges, two time-horizons are required.  The first is the inclusion horizon, or the age at which events are no longer considered.  This will allow, for instance, limiting circulation calculations to only the past two years.  The second is the importance aging horizon, which will allow recent events to be considered more important than those further in the past.   A value of zero for this horizon will mean that all events are seen to be of equal importance.  These are presented as text inputs that will accept interval strings such as "2 years", "30 days", or "6 weeks, 2 days".  Numeric values without a timespan qualifier are considered to be a number of seconds.
+
+For those badges that have the Fixed Rating flag set, no statistical input is gathered for records in the population of the badge definition.  Instead, all records in the population are assigned this fixed value for the badge.
+
+Rating process
+++++++++++++++
+
+For badges other than those with the Fixed Rating set, the collected statistical input parameters are used to derive the mean, median, mode, min, max, and standard deviation values for the bibliographic record population. Each record passing the requisite thresholds are assigned a badge-specific value based on the quintile into which the value falls.  This value, interpreted as a one-to-five rating, is stored for static application, instead of being calculated on the fly, when the badge is in scope and the record is part of a search result set.  Thus records with values in the bottom quintile will have a rating of one, and records with values in the top quintile will have a rating of five.
+
+All badge values for all records are calculated by a secondary process that runs in the background on the server on a regular basis.
+
+Display in the OPAC
++++++++++++++++++++
+
+Ratings are displayed in two places within the OPAC.  Like the rest of the TPAC, this is templated and display can be modified or disabled through customization.
+
+First, on the record result page, the overall average popularity rating is displayed with a label of "Popularity" below the record-specific data such as call number and publisher, and above the holdings counts.
+
+Second, on the record detail page, the list of badge names earned by the record that are in scope for the search location, and the 1-5 rating value of each badge, is displayed in a horizontal list above the copy detail table.
+
+Future possibilities
+++++++++++++++++++++
+
+This infrastructure will directly support future work such as Patron and Staff ratings, and even allow search and browse filtering based  on specific badges and ratings.  Additionally, bibliographic information could be leveraged to create metadata badges that would affect the ranking of record, in addition to the non-bibliographic badges described here.
+
+Performance
++++++++++++
+
+It is expected that there may be some very small speed impact, but all attempts have been made to mitigate this by precalculating the adjustment values and by keeping the search-required data as compact as possible.  By doing this, the aggregate cost per search should be extremely small.  In addition, the development will include a setting to define the amount of database resources to dedicate to the job of badge value calculation and reduce its run time.
+

commit 4f48f276d052a6211be625d3b87d8077a789999c
Author: Mike Rylander <mrylander at gmail.com>
Date:   Tue Apr 5 12:10:53 2016 -0400

    LP#1549505: PGTap test to show the infrastructure is working
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/sql/Pg/t/regress/lp1549505_statistical_popularity_infrastructure.pg b/Open-ILS/src/sql/Pg/t/regress/lp1549505_statistical_popularity_infrastructure.pg
new file mode 100644
index 0000000..e5d44b7
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/t/regress/lp1549505_statistical_popularity_infrastructure.pg
@@ -0,0 +1,33 @@
+BEGIN;
+
+SELECT plan(1);
+
+INSERT INTO config.bib_source (id,        source       )
+	VALUES                (999999998, 'test source');
+INSERT INTO rating.badge (id,        name,         scope, popularity_parameter, src_filter)
+	VALUES           (999999998, 'test badge', 1,     15,                   999999998 );
+
+INSERT INTO biblio.record_entry (id,        source,    last_xact_id, marc)
+VALUES                          (999999998, 999999998, 'pgtap',      $$<record
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.loc.gov/MARC21/slim http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd"
+    xmlns="http://www.loc.gov/MARC21/slim">
+  <leader>00531nam a2200157 a 4500</leader>
+  <controlfield tag="005">20080729170300.0</controlfield>
+  <controlfield tag="008">      t19981999enka              0 eng  </controlfield>
+  <datafield tag="245" ind1="1" ind2="4">
+    <subfield code="a">test record</subfield>
+  </datafield>
+</record>$$);
+
+SELECT rating.recalculate_badge_score(999999998);
+
+SELECT is(
+    score, 5,
+    'LP#1549505: Badge caclulation framework is operational'
+)
+FROM rating.record_badge_score;
+WHERE record = 999999998
+AND badge = 999999998;
+
+ROLLBACK;

commit 1fd8103bf33430fe0b4b5ae8d2b2b051802ac473
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Fri Mar 11 13:27:03 2016 -0500

    LP#1549505: add flag to tweak popularity-adjusted relevance
    
    This adds a new global_flag, search.max_popularity_importance_multiplier,
    to control the factor by which popularity affects Popularity Adjusted
    Relevance ranking.
    
    The value should be a decimal number, typically between 1.0 and 2.0:
    
    * 1.0 be would be equivalent to not adjusting relevance for popularity
      at all.
    * 1.1 would mean that the multiplier would range from 1 (for zero
      popularity) to 1.1 (for maximum popularity), for a maximum boost of
      10% of the base relevance value of the record.
    
    What's happening in the code:
    
    Scale the 0-5 effect of popularity badges by providing a multiplier
    for the badge average based on the overall maximum multiplier.  Two
    examples, comparing the effect to the default $max_mult value of 2.0,
    which causes a $adjusted_scale value of 0.2:
    
     * Given the default $max_mult of 2.0, the value of
       $adjusted_scale will be 0.2 [($max_mult - 1.0) / 5.0].
       For a record whose average badge score is the maximum
       of 5.0, that would make the relevance multiplier be
       2.0:
          1.0 + (5.0 [average score] * 0.2 [ $adjusted_scale ],
       This would have the effect of doubling the effective
       relevance of highly popular items.
    
     * Given a $max_mult of 1.1, the value of $adjusted_scale
       will be 0.02, meaning that the average badge value will be
       multiplied by 0.02 rather than 0.2, then added to 1.0 and
       used as a multiplier against the base relevance.  Thus a
       change of at most 10% to the base relevance for a record
       with a 5.0 average badge score. This will allow records
       that are naturally very relevant to avoid being pushed
       below badge-heavy records.
    
     * Given a $max_mult of 3.0, the value of $adjusted_scale
       will be 0.4, meaning that the average badge value will be
       multiplied by 0.4 rather than 0.2, then added to 1.0 and
       used as a multiplier against the base relevance. Thus a
       change of as much as 200% to (or three times the size of)
       the base relevance for a record with a 5.0 average badge
       score.  This in turn will cause badges to outweigh
       relevance to a very large degree.
    
    The maximum badge multiplier can be set to a value less than
    1.0; this would have the effect of making less popular items
    show up higher in the results.  While this is not a likely
    option for production use, it could be useful for identifying
    interesting long-tail hits, particularly in a database
    where enough badges are configured so that very few records
    have an average badge score of zero.
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    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/Application/Storage/Driver/Pg/QueryParser.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Driver/Pg/QueryParser.pm
index f06d097..9265a88 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Driver/Pg/QueryParser.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Driver/Pg/QueryParser.pm
@@ -126,6 +126,14 @@ sub default_preferred_language_multiplier {
     return $self->custom_data->{default_preferred_language_multiplier};
 }
 
+sub max_popularity_importance_multiplier {
+    my $self = shift;
+    my $max = shift;
+
+    $self->custom_data->{max_popularity_importance_multiplier} = $max if defined($max);
+    return $self->custom_data->{max_popularity_importance_multiplier};
+}
+
 sub simple_plan {
     my $self = shift;
 
@@ -884,7 +892,60 @@ sub toSQL {
     } elsif ($sort_filter eq 'edit_date') {
         $rank = "FIRST((SELECT edit_date FROM biblio.record_entry rbr WHERE rbr.id = m.source))";
     } elsif ($sort_filter eq 'poprel') {
-        $rank = '1.0/((' . $rel . ') * (1.0 + AVG(COALESCE(pop_with.total_score::NUMERIC,0.0)) / 5.0))::NUMERIC';
+        my $max_mult = $self->QueryParser->max_popularity_importance_multiplier() // 2.0;
+        $max_mult = 0.1 if $max_mult < 0.1; # keep it within reasonable bounds,
+                                            # and avoid the division-by-zero error
+                                            # you'd get if you allowed it to be
+                                            # zero
+
+        if ( $max_mult == 1.0 ) { # no adjustment requested by the configuration
+            $rank = "1.0/($rel)::NUMERIC";
+        } else { # calculate adjustment
+
+            # Scale the 0-5 effect of popularity badges by providing a multiplier
+            # for the badge average based on the overall maximum
+            # multiplier.  Two examples, comparing the effect to the default
+            # $max_mult value of 2.0, which causes a $adjusted_scale value
+            # of 0.2:
+            #
+            #  * Given the default $max_mult of 2.0, the value of
+            #    $adjusted_scale will be 0.2 [($max_mult - 1.0) / 5.0].
+            #    For a record whose average badge score is the maximum
+            #    of 5.0, that would make the relevance multiplier be
+            #    2.0:
+            #       1.0 + (5.0 [average score] * 0.2 [ $adjusted_scale ],
+            #    This would have the effect of doubling the effective
+            #    relevance of highly popular items.
+            #
+            #  * Given a $max_mult of 1.1, the value of $adjusted_scale
+            #    will be 0.02, meaning that the average badge value will be
+            #    multiplied by 0.02 rather than 0.2, then added to 1.0 and
+            #    used as a multiplier against the base relevance.  Thus a
+            #    change of at most 10% to the base relevance for a record
+            #    with a 5.0 average badge score. This will allow records
+            #    that are naturally very relevant to avoid being pushed
+            #    below badge-heavy records.
+            #
+            #  * Given a $max_mult of 3.0, the value of $adjusted_scale
+            #    will be 0.4, meaning that the average badge value will be
+            #    multiplied by 0.4 rather than 0.2, then added to 1.0 and
+            #    used as a multiplier against the base relevance. Thus a
+            #    change of as much as 200% to (or three times the size of)
+            #    the base relevance for a record with a 5.0 average badge
+            #    score.  This in turn will cause badges to outweigh
+            #    relevance to a very large degree.
+            #
+            # The maximum badge multiplier can be set to a value less than
+            # 1.0; this would have the effect of making less popular items
+            # show up higher in the results.  While this is not a likely
+            # option for production use, it could be useful for identifying
+            # interesting long-tail hits, particularly in a database
+            # where enough badges are configured so that very few records
+            # have an overage badge score of zero.
+
+            my $adjusted_scale = ( $max_mult - 1.0 ) / 5.0;
+            $rank = "1.0/(( $rel ) * (1.0 + (AVG(COALESCE(pop_with.total_score::NUMERIC,0.0)) * $adjusted_scale)))::NUMERIC";
+        }
     } elsif ($sort_filter =~ /^pop/) {
         $rank = '1.0/(AVG(COALESCE(pop_with.total_score::NUMERIC,0.0)) + 5.0)::NUMERIC';
         my $pop_desc = $desc eq 'ASC' ? 'DESC' : 'ASC';
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 aebd348..debc865 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
@@ -5,6 +5,7 @@ use OpenSRF::EX qw/:try/;
 use OpenILS::Application::Storage::FTS;
 use OpenILS::Utils::Fieldmapper;
 use OpenSRF::Utils::Logger qw/:level/;
+use OpenILS::Application::AppUtils;
 use OpenSRF::Utils::Cache;
 use OpenSRF::Utils::JSON;
 use Data::Dumper;
@@ -12,6 +13,8 @@ use Digest::MD5 qw/md5_hex/;
 
 use OpenILS::Application::Storage::QueryParser;
 
+my $U = 'OpenILS::Application::AppUtils';
+
 my $log = 'OpenSRF::Utils::Logger';
 
 $VERSION = 1;
@@ -70,6 +73,16 @@ sub _initialize_parser {
             )->gather(1),
     );
 
+    my $max_mult;
+    my $cgf = $cstore->request(
+        'open-ils.cstore.direct.config.global_flag.retrieve',
+        'search.max_popularity_importance_multiplier'
+    )->gather(1);
+    $max_mult = $cgf->value if $cgf && $U->is_true($cgf->enabled);
+    $max_mult //= 2.0;
+    $max_mult = 2.0 unless $max_mult =~ /^-?(?:\d+\.?|\.\d)\d*\z/; # just in case
+    $parser->max_popularity_importance_multiplier($max_mult);
+
     $cstore->disconnect;
     die("Cannot initialize $parser!") unless ($parser->initialization_complete);
 }
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 31974ca..feb6cb4 100644
--- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql
+++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
@@ -11724,6 +11724,18 @@ INSERT INTO config.global_flag (name, label, value, enabled) VALUES (
     '',
     TRUE
 );
+
+INSERT INTO config.global_flag (name, label, value, enabled) VALUES (
+    'search.max_popularity_importance_multiplier',
+    oils_i18n_gettext(
+        'search.max_popularity_importance_multiplier',
+        'Maximum popularity importance multiplier for popularity-adjusted relevance searches (decimal value between 1.0 and 2.0)',
+        'cgf',
+        'label'
+    ),
+    '2.0',
+    TRUE
+);
 */
 
 INSERT INTO config.usr_setting_type (name,opac_visible,label,description,datatype)
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.statisitcal-ratings.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.statisitcal-ratings.sql
index 1bcbe63..d6e5a22 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.statisitcal-ratings.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.statisitcal-ratings.sql
@@ -135,6 +135,18 @@ INSERT INTO config.global_flag (name, label, value, enabled) VALUES (
     TRUE
 );
 
+INSERT INTO config.global_flag (name, label, value, enabled) VALUES (
+    'search.max_popularity_importance_multiplier',
+    oils_i18n_gettext(
+        'search.max_popularity_importance_multiplier',
+        'Maximum popularity importance multiplier for popularity-adjusted relevance searches (decimal value between 1.0 and 2.0)',
+        'cgf',
+        'label'
+    ),
+    '2.0',
+    TRUE
+);
+
 CREATE TABLE rating.popularity_parameter (
     id          INT     PRIMARY KEY,
     name        TEXT    NOT NULL UNIQUE, -- i18n

commit d4d2f42e8674f93284d2932e171bf9a4f9cc8cf4
Author: Galen Charlton <gmc at esilibrary.com>
Date:   Wed Feb 24 17:19:14 2016 -0500

    LP#1549505: add admin interface to manage badges
    
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml
index 6f86b51..9ab8eef 100644
--- a/Open-ILS/examples/fm_IDL.xml
+++ b/Open-ILS/examples/fm_IDL.xml
@@ -304,7 +304,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 			<field reporter:label="Importance Horizon" name="importance_age" reporter:datatype="text"/>
 			<field reporter:label="Importance Interval" name="importance_interval" reporter:datatype="text"/>
 			<field reporter:label="Importance Scale" name="importance_scale" reporter:datatype="text"/>
-			<field reporter:label="Percentile" name="percentile" reporter:datatype="int"/>
+			<field reporter:label="Percentile" name="percentile" reporter:datatype="float"/>
 			<field reporter:label="Attribute Filter" name="attr_filter" reporter:datatype="text"/>
 			<field reporter:label="Circ Mod Filter" name="circ_mod_filter" reporter:datatype="link"/>
 			<field reporter:label="Bib Source Filter" name="src_filter" reporter:datatype="link"/>
diff --git a/Open-ILS/src/templates/staff/admin/local/rating/badge.tt2 b/Open-ILS/src/templates/staff/admin/local/rating/badge.tt2
new file mode 100644
index 0000000..a67249d
--- /dev/null
+++ b/Open-ILS/src/templates/staff/admin/local/rating/badge.tt2
@@ -0,0 +1,60 @@
+[%
+  WRAPPER 'staff/base.tt2';
+  ctx.page_type = l('Statistical Popularity Badges');
+  ctx.page_app = 'egAdminRating';
+  ctx.page_ctrl = 'Badges';
+%]
+
+[% BLOCK APP_JS %]
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/grid.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/ui.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/admin/local/rating/badge.js"></script>
+<link rel="stylesheet" href="[% ctx.base_path %]/staff/css/admin.css" />
+<script>
+  angular.module('egCoreMod').run(['egStrings', function(s) {
+    s.CONFIRM_DELETE_BADGE_TITLE =
+      "[% l('Confirm badge deletion') %]";
+    s.CONFIRM_DELETE_BADGE_BODY =
+      "[% l('Delete badge {{id}} ({{name}})?') %]";
+  }]);
+</script>
+[% END %]
+
+<div class="container-fluid" style="text-align:center">
+  <div class="alert alert-info alert-less-pad strong-text-2">
+    [% l('Statistical Popularity Badges') %]
+  </div>
+</div>
+
+<!--
+<div class="row">
+  <div class="col-md-4">
+    <div class="form-group">
+      <label>[% l('Scope Library') %]</label>
+      <eg-org-selector onchange="org_changed" 
+        selected="context_org"></eg-org-selector>
+    </div>
+  </div>
+</div>
+-->
+
+<eg-grid
+    id-field="id"
+    idl-class="rb"
+    grid-controls="gridControls"
+    features="-multiselect"
+    persist-key="admin.local.rating.badge">
+   
+    <eg-grid-action label="[% l('Add badge') %]" handler="create_rb"></eg-grid-action> 
+    <eg-grid-action label="[% l('Edit badge') %]" handler="update_rb"></eg-grid-action> 
+    <eg-grid-action label="[% l('Delete badge') %]" handler="delete_rb"></eg-grid-action> 
+
+    <eg-grid-field label="[% l('Name') %]"                   path='name'></eg-grid-field>
+    <eg-grid-field label="[% l('Description') %]"            path='description'></eg-grid-field>
+    <eg-grid-field label="[% l('Scope') %]"                  path='scope.name'></eg-grid-field>
+    <eg-grid-field label="[% l('Popularity Parameter') %]"   path='popularity_parameter.name'></eg-grid-field>
+    <eg-grid-field label="[% l('ID') %]" path='id' required hidden></eg-grid-field>
+    <eg-grid-field path='*' hidden></eg-grid-field> 
+</eg-grid>
+
+[% END %]
diff --git a/Open-ILS/src/templates/staff/admin/local/rating/edit_badge.tt2 b/Open-ILS/src/templates/staff/admin/local/rating/edit_badge.tt2
new file mode 100644
index 0000000..9a1426b
--- /dev/null
+++ b/Open-ILS/src/templates/staff/admin/local/rating/edit_badge.tt2
@@ -0,0 +1,42 @@
+<form ng-submit="ok(record)" role="form" class="form-validated">
+    <div class="modal-header">
+      <button type="button" class="close" ng-click="cancel()" 
+        aria-hidden="true">×</button>
+      <h4 class="modal-title">{{record_label}}</h4>
+    </div>
+    <div class="modal-body">
+      <div class="form-group row" ng-repeat="field in fields | filter:{virtual:'!true'}">
+        <div class="col-md-3">
+          <label for="rec-{{field.name}}">{{field.label}}</label>
+        </div>
+        <div class="col-md-9">
+            <span  ng-if="field.datatype == 'id' || field.readonly">{{record[field.name]}}</span>
+            <eg-org-selector ng-if="field.datatype == 'org_unit'"
+                selected="record[field.name + '_ou'].org" 
+                onchange="record[field.name + '_ou'].update_org">
+            </eg-org-selector>
+            <input ng-if="field.datatype == 'text'"
+                ng-required="field.is_required(record)"
+                ng-model="record[field.name]"></input>
+            <input ng-if="field.datatype == 'int'" type="number"
+                ng-required="field.is_required(record)"
+                ng-model="record[field.name]"></input>
+            <input ng-if="field.datatype == 'float'" type="number" step="0.1"
+                ng-required="field.is_required(record)"
+                ng-model="record[field.name]"></input>
+            <span ng-if="field.datatype == 'link'" class="nullable">
+            <select ng-if="field.datatype == 'link'"
+                ng-options="item.id as item.name for item in field.linked_values"
+                ng-model="record[field.name]">
+                    <option value=""></option>
+                </select>
+            </span>
+        </div>
+      </div>
+    </div>
+    <div class="modal-footer">
+      <input type="submit" class="btn btn-primary" value="[% l('OK') %]"/>
+      <button class="btn btn-warning" ng-click="cancel($event)">[% l('Cancel') %]</button>
+    </div>
+  </div> <!-- modal-content -->
+</form>
diff --git a/Open-ILS/src/templates/staff/admin/local/t_splash.tt2 b/Open-ILS/src/templates/staff/admin/local/t_splash.tt2
index 8dcb28f..f6b7da3 100644
--- a/Open-ILS/src/templates/staff/admin/local/t_splash.tt2
+++ b/Open-ILS/src/templates/staff/admin/local/t_splash.tt2
@@ -31,6 +31,7 @@
     ,[ l('Search Filter Groups'), "./admin/local/actor/search_filter_group" ]
     ,[ l('Standing Penalties'), "./admin/local/config/standing_penalty" ]
     ,[ l('Statistical Categories Editor'), "./admin/local/asset/stat_cat_editor" ]
+    ,[ l('Statistical Popularity Badges'), "./admin/local/rating/badge" ]
    ];
 
    USE table(interfaces, rows=9);
diff --git a/Open-ILS/src/templates/staff/css/style.css.tt2 b/Open-ILS/src/templates/staff/css/style.css.tt2
index c2ff924..d4e4ae5 100644
--- a/Open-ILS/src/templates/staff/css/style.css.tt2
+++ b/Open-ILS/src/templates/staff/css/style.css.tt2
@@ -97,6 +97,9 @@
 .form-validated input.ng-valid.ng-dirty {
   background-color: #78FA89;
 }
+.form-validated input.ng-invalid-required {
+  background-color: #FACDCF;
+}
 
 /* --------------------------------------------------------------------------
  * Local style
diff --git a/Open-ILS/web/js/ui/default/staff/admin/local/rating/badge.js b/Open-ILS/web/js/ui/default/staff/admin/local/rating/badge.js
new file mode 100644
index 0000000..3df43e9
--- /dev/null
+++ b/Open-ILS/web/js/ui/default/staff/admin/local/rating/badge.js
@@ -0,0 +1,189 @@
+angular.module('egAdminRating',
+    ['ngRoute','ui.bootstrap','egCoreMod','egUiMod','egGridMod'])
+
+.controller('Badges',
+       ['$scope','$q','$timeout','$location','$window','$modal',
+        'egCore','egGridDataProvider','egConfirmDialog',
+function($scope , $q , $timeout , $location , $window , $modal ,
+         egCore , egGridDataProvider , egConfirmDialog) {
+
+    egCore.startup.go();
+
+    function get_record_label() {
+        return egCore.idl.classes['rb'].label;
+    }
+
+    function flatten_linked_values(cls, list) {
+        var results = [];
+        var fields = egCore.idl.classes[cls].fields;
+        var id_field;
+        var selector;
+        angular.forEach(fields, function(fld) {
+            if (fld.datatype == 'id') {
+                id_field = fld.name;
+                selector = fld.selector ? fld.selector : id_field;
+                return;
+            }
+        });
+        angular.forEach(list, function(item) {
+            var rec = egCore.idl.toHash(item);
+            results.push({
+                id : rec[id_field],
+                name : rec[selector]
+            });
+        });
+        return results;
+    }
+
+    horizon_required = {};
+    percentile_required = {};
+    function get_popularity_with_required() {
+        egCore.pcrud.retrieveAll(
+            'rp', {}, {atomic : true}
+        ).then(function(list) {
+            angular.forEach(list, function(item) {
+                horizon_required[item.id()] = item.require_horizon();
+                percentile_required[item.id()] = item.require_percentile();
+            });
+        });
+    }
+
+    function get_field_list(rec) {
+        var fields = egCore.idl.classes['rb'].fields;
+
+        var promises = [];
+        // flesh selectors
+        angular.forEach(fields, function(fld) {
+            if (fld.datatype == 'link') {
+                egCore.pcrud.retrieveAll(
+                    fld.class, {}, {atomic : true}
+                ).then(function(list) {
+                    fld.linked_values = flatten_linked_values(fld.class, list);
+                });
+            }
+            if (fld.datatype == 'org_unit') {
+                rec[fld.name + '_ou'] = {};
+                rec[fld.name + '_ou']['org'] = egCore.org.get(rec[fld.name]);
+                rec[fld.name + '_ou']['update_org'] = function(org) {
+                    rec[fld.name] = org.id();
+                };
+            }
+            if (fld.name == 'last_calc') {
+                fld['readonly'] = true;
+            }
+            fld.is_required = function(record) {
+                return false;
+            };
+            if (fld.name == 'name') {
+                fld.is_required = function(record) {
+                    return true;
+                };
+            }
+            if (fld.name == 'horizon_age') {
+                fld.is_required = function(record) {
+                    return horizon_required[record['popularity_parameter']] == 't';
+                };
+            }
+            if (fld.name == 'percentile') {
+                fld.is_required = function(record) {
+                    return percentile_required[record['popularity_parameter']] == 't';
+                };
+            }
+        });
+        return fields;
+    }
+
+    function spawn_editor(rb, action) {
+        var deferred = $q.defer();
+        $modal.open({
+            templateUrl: './admin/local/rating/edit_badge',
+            controller:
+                ['$scope', '$modalInstance', function($scope, $modalInstance) {
+                $scope.focusMe = true;
+                $scope.record = egCore.idl.toHash(rb);
+                // non-integer numeric field require parseFloat
+                $scope.record.percentile = parseFloat($scope.record.percentile);
+                $scope.record_label = get_record_label();
+                $scope.fields = get_field_list($scope.record);
+                get_popularity_with_required();
+                $scope.ok = function(args) { $modalInstance.close(args) }
+                $scope.cancel = function () { $modalInstance.dismiss() }
+            }]
+        }).result.then(function(args) {
+            var rb = new egCore.idl.rb();
+            if (action == 'update') rb.id(args.id);
+            rb.name(args.name);
+            rb.description(args.description);
+            rb.scope(args.scope);
+            rb.weight(args.weight);
+            rb.horizon_age(args.horizon_age);
+            rb.importance_age(args.importance_age);
+            rb.importance_interval(args.importance_interval);
+            rb.importance_scale(args.importance_scale);
+            rb.recalc_interval(args.recalc_interval);
+            rb.attr_filter(args.attr_filter);
+            rb.src_filter(args.src_filter);
+            rb.circ_mod_filter(args.circ_mod_filter);
+            rb.loc_grp_filter(args.loc_grp_filter);
+            rb.popularity_parameter(args.popularity_parameter);
+            rb.fixed_rating(args.fixed_rating);
+            rb.percentile(args.percentile);
+            rb.discard(args.discard);
+            rb.last_calc(args.last_calc);
+            if (action == 'create') {
+                egCore.pcrud.create(rb).then(function() { deferred.resolve(); });
+            } else {
+                egCore.pcrud.update(rb).then(function() { deferred.resolve(); });
+            }
+        });
+        return deferred.promise;
+    }
+
+    $scope.create_rb = function() {
+        var rb = new egCore.idl.rb();
+
+        // make sure an OU is selected by default
+        rb.scope(egCore.auth.user().ws_ou());
+
+        spawn_editor(rb, 'create').then(function() {
+            $scope.gridControls.refresh();
+        });
+    }
+
+    $scope.update_rb = function(selected) {
+        if (!selected || !selected.length) return;
+
+        egCore.pcrud.retrieve('rb', selected[0].id).then(function(rb) {
+            spawn_editor(rb, 'update').then(function() {
+                $scope.gridControls.refresh();
+            });
+        });
+    }
+
+    $scope.delete_rb = function(selected) {
+        if (!selected || !selected.length) return;
+
+        egCore.pcrud.retrieve('rb', selected[0].id).then(function(rb) {
+            egConfirmDialog.open(
+                egCore.strings.CONFIRM_DELETE_BADGE_TITLE,
+                egCore.strings.CONFIRM_DELETE_BADGE_BODY,
+                { name : rb.name(), id : rb.id() }
+            ).result.then(function() {
+                egCore.pcrud.remove(rb).then(function() {
+                    $scope.gridControls.refresh();
+                });
+            });            
+        });
+    }
+
+    $scope.gridControls = {
+        activateItem : function (item) {
+            $scope.update_rb([item]);
+        },
+        setQuery : function() {
+            return {
+                'id' : {'!=' : null}
+            }
+        }
+    }
+}])

commit 701d069268f75620e145ea73a2e21aec7ec5db42
Author: Mike Rylander <mrylander at gmail.com>
Date:   Thu Feb 4 17:19:03 2016 -0500

    LP#1549505: Add ability to set default sorter via global flag
    
    In addition, support for sorting by ascending popularity remains
    in the back end, however, since if one's configured badges such
    that almost every record has a badge score, that sort order could
    be useful for examining the long tail of the collection.
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm
index b1527fb..bd9c3df 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm
@@ -256,6 +256,11 @@ sub load_common {
         return $self->redirect_ssl unless $self->cgi->https;
     }
 
+    # XXX Cache this? Makes testing difficult as apache needs a restart.
+    my $default_sort = $e->retrieve_config_global_flag('opac.default_sort');
+    $ctx->{default_sort} =
+        ($default_sort && $U->is_true($default_sort->enabled)) ? $default_sort->value : '';
+
     $ctx->{referer} = $self->cgi->referer;
     $ctx->{path_info} = $self->cgi->path_info;
     $ctx->{full_path} = $ctx->{base_path} . $self->cgi->path_info;
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 9c59fa3..31974ca 100644
--- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql
+++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
@@ -11712,6 +11712,20 @@ INSERT INTO config.global_flag (name, label, value, enabled) VALUES (
     TRUE
 );
 
+/* To be added when upgrade scripts are pulled into the baseline
+INSERT INTO config.global_flag (name, label, value, enabled) VALUES (
+    'opac.default_sort',
+    oils_i18n_gettext(
+        'opac.default_sort',
+        'OPAC Default Sort (titlesort, authorsort, pubdate, popularity, poprel, or empty)',
+        'cgf',
+        'label'
+    ),
+    '',
+    TRUE
+);
+*/
+
 INSERT INTO config.usr_setting_type (name,opac_visible,label,description,datatype)
     VALUES (
         'history.circ.retention_age',
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.statisitcal-ratings.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.statisitcal-ratings.sql
index 1c9324d..1bcbe63 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.statisitcal-ratings.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.statisitcal-ratings.sql
@@ -128,6 +128,13 @@ CREATE TRIGGER reporter_hold_request_record_trigger AFTER INSERT OR UPDATE ON ac
 
 CREATE SCHEMA rating;
 
+INSERT INTO config.global_flag (name, label, value, enabled) VALUES (
+    'opac.default_sort',
+    'OPAC Default Sort (titlesort, authorsort, pubdate, popularity, poprel, or empty for relevance)',
+    '',
+    TRUE
+);
+
 CREATE TABLE rating.popularity_parameter (
     id          INT     PRIMARY KEY,
     name        TEXT    NOT NULL UNIQUE, -- i18n
diff --git a/Open-ILS/src/templates/opac/parts/advanced/search.tt2 b/Open-ILS/src/templates/opac/parts/advanced/search.tt2
index 8cfad7f..dc650a1 100644
--- a/Open-ILS/src/templates/opac/parts/advanced/search.tt2
+++ b/Open-ILS/src/templates/opac/parts/advanced/search.tt2
@@ -118,9 +118,13 @@
                             [% END %]
 [%
                 CASE "sort_selector";
+                    default_sort=ctx.default_sort;
+                    IF CGI.param('sort');
+                        default_sort=CGI.param('sort');
+                    END;
                     INCLUDE "opac/parts/filtersort.tt2"
                         id=adv_chunk.id
-                        value=CGI.param('sort') class='results_header_sel';
+                        value=default_sort class='results_header_sel';
                     %]
 
                     [% IF NOT metarecords.disabled %]
diff --git a/Open-ILS/src/templates/opac/parts/filtersort.tt2 b/Open-ILS/src/templates/opac/parts/filtersort.tt2
index 1907058..2990352 100644
--- a/Open-ILS/src/templates/opac/parts/filtersort.tt2
+++ b/Open-ILS/src/templates/opac/parts/filtersort.tt2
@@ -14,4 +14,16 @@
         <option value='pubdate.descending'[% value == 'pubdate.descending' ? ' selected="selected"' : '' %]>[% l("Date: Newest to Oldest") %]</option>
         <option value='pubdate'[% value == 'pubdate' ? ' selected="selected"' : '' %]>[% l("Date: Oldest to Newest") %]</option>
     </optgroup>
+    <optgroup label='[% l("Sort by Popularity") %]'>
+        <option value='popularity'[% value == 'popularity' ? ' selected="selected"' : '' %]>[% l("Most Popular") %]</option>
+<!-- Sorting by least popular items first is probably not an expected
+     choice in production, but could be useful in cases where every
+     record has at least one badge value assigned and you want
+     to investigate the long tail.
+-->
+<!--
+        <option value='popularity.descending'[% value == 'popularity.descending' ? ' selected="selected"' : '' %]>[% l("Least Popular") %]</option>
+-->
+        <option value='poprel'[% value == 'poprel' ? ' selected="selected"' : '' %]>[% l("Popularity Adjusted Relevance") %]</option>
+    </optgroup>
 </select>
diff --git a/Open-ILS/src/templates/opac/parts/searchbar.tt2 b/Open-ILS/src/templates/opac/parts/searchbar.tt2
index 20088fa..a48c4a0 100644
--- a/Open-ILS/src/templates/opac/parts/searchbar.tt2
+++ b/Open-ILS/src/templates/opac/parts/searchbar.tt2
@@ -98,7 +98,12 @@
         [% END %]
     </div>
     [%- END %]
-    [% UNLESS took_care_of_form %]</form>[% END %]
+    [% UNLESS took_care_of_form %]
+        [% IF ctx.default_sort %]
+            <input type="hidden" name="sort" value="[% ctx.default_sort %]"/>
+        [% END %]
+        </form>
+    [% END %]
     [% IF (is_advanced AND NOT is_special) AND CGI.param('qtype') %]
     <div class="opac-auto-102">
         [ <a href="[% mkurl(ctx.opac_root _ '/advanced') %]">[%

commit b6dd839f6c292849017cdc8a2f0ff590facd70cc
Author: Mike Rylander <mrylander at gmail.com>
Date:   Wed Feb 3 15:46:52 2016 -0500

    LP#1549505: Provide a cron-able script to perform badge recalculation
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/biblio.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/biblio.pm
index c285489..3f52baf 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/biblio.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/biblio.pm
@@ -185,6 +185,33 @@ __PACKAGE__->register_method(
     api_level   => 1,
 );
 
+
+sub regenerate_badge_list {
+    my $self = shift;
+    my $client = shift;
+
+    my $sth = biblio::record_entry->db_Main->prepare_cached( <<"    SQL" );
+        SELECT  r.id AS badge
+          FROM  rating.badge r
+          WHERE r.last_calc < NOW() - r.recalc_interval
+                OR r.last_calc IS NULL
+          ORDER BY r.last_calc ASC NULLS FIRST -- oldest first
+    SQL
+
+    $sth->execute;
+    while ( my $row = $sth->fetchrow_hashref ) {
+        $client->respond( $row->{badge} );
+    }
+    return undef;
+}
+__PACKAGE__->register_method(
+    api_name    => 'open-ils.storage.biblio.regenerate_badge_list',
+    method      => 'regenerate_badge_list',
+    api_level   => 1,
+    cachable    => 1,
+);
+
+
 sub record_by_barcode {
     my $self = shift;
     my $client = shift;
diff --git a/Open-ILS/src/support-scripts/badge_score_generator.pl b/Open-ILS/src/support-scripts/badge_score_generator.pl
new file mode 100755
index 0000000..5196b48
--- /dev/null
+++ b/Open-ILS/src/support-scripts/badge_score_generator.pl
@@ -0,0 +1,60 @@
+#!/usr/bin/perl
+# ---------------------------------------------------------------------
+# Badge score generator
+# ./badge_score_generator.pl <bootstrap_config> <lockfile>
+# ---------------------------------------------------------------------
+
+use strict; 
+use warnings;
+use OpenSRF::Utils::JSON;
+use OpenSRF::System;
+use OpenSRF::Utils::SettingsClient;
+use OpenSRF::MultiSession;
+
+my $config = shift || die "bootstrap config required\n";
+my $lockfile = shift || "/tmp/generate_badge_scores-LOCK";
+
+if (-e $lockfile) {
+        open(F,$lockfile);
+        my $pid = <F>;
+        close F;
+
+        open(F,'/bin/ps axo pid|');
+        while ( my $p = <F>) {
+                chomp($p);
+                if ($p =~ s/\s*(\d+)$/$1/o && $p == $pid) {
+                        die "I seem to be running already at pid $pid.  If not, try again\n";
+                }
+        }
+        close F;
+}
+
+open(F, ">$lockfile");
+print F $$;
+close F;
+
+OpenSRF::System->bootstrap_client( config_file => $config );
+my $settings = OpenSRF::Utils::SettingsClient->new;
+my $parallel = $settings->config_value( badge_score_generator => 'parallel' ) || 1; 
+
+my $multi_generator = OpenSRF::MultiSession->new(
+    app => 'open-ils.cstore', 
+    cap => $parallel, 
+    api_level => 1,
+);
+
+my $storage = OpenSRF::AppSession->create("open-ils.storage");
+my $r = $storage->request('open-ils.storage.biblio.regenerate_badge_list');
+
+while (my $resp = $r->recv) {
+    my $badge_id = $resp->content;
+    $multi_generator->request(
+        'open-ils.cstore.json_query',
+        { from => [ 'rating.recalculate_badge_score' => $badge_id ] }
+    );
+}
+$storage->disconnect();
+$multi_generator->session_wait(1);
+$multi_generator->disconnect;
+
+unlink $lockfile;

commit 8180b60d040d6b1025de21bbd587839076037493
Author: Mike Rylander <mrylander at gmail.com>
Date:   Fri Jan 22 17:13:25 2016 -0500

    LP#1549505: Teach QP and its caller stack how to use badges
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

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 a337619..5c07790 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Biblio.pm
@@ -1364,9 +1364,9 @@ sub staged_search {
 
             # Create backwards-compatible result structures
             if($IAmMetabib) {
-                $results = [map {[$_->{id}, $_->{rel}, $_->{record}]} @$results];
+                $results = [map {[$_->{id}, $_->{badges}, $_->{popularity}, $_->{rel}, $_->{record}]} @$results];
             } else {
-                $results = [map {[$_->{id}]} @$results];
+                $results = [map {[$_->{id}, $_->{badges}, $_->{popularity}]} @$results];
             }
 
             push @$new_ids, grep {defined($_)} map {$_->[0]} @$results;
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Driver/Pg/QueryParser.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Driver/Pg/QueryParser.pm
index ea55769..f06d097 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Driver/Pg/QueryParser.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Driver/Pg/QueryParser.pm
@@ -651,6 +651,8 @@ __PACKAGE__->add_search_filter( 'statuses' );
 __PACKAGE__->add_search_filter( 'locations' );
 __PACKAGE__->add_search_filter( 'location_groups' );
 __PACKAGE__->add_search_filter( 'bib_source' );
+__PACKAGE__->add_search_filter( 'badge_orgs' );
+__PACKAGE__->add_search_filter( 'badges' );
 __PACKAGE__->add_search_filter( 'site' );
 __PACKAGE__->add_search_filter( 'pref_ou' );
 __PACKAGE__->add_search_filter( 'lasso' );
@@ -808,7 +810,6 @@ sub toSQL {
         $rel = "($rel * COALESCE( NULLIF( FIRST(mrv.vlist \@> ARRAY[lang_with.id]), FALSE )::INT * $plw, 1))";
         $$flat_plan{uses_mrv} = 1;
     }
-    $rel = "1.0/($rel)::NUMERIC";
 
     my $mrv_join = '';
     if ($$flat_plan{uses_mrv}) {
@@ -831,23 +832,66 @@ sub toSQL {
         $bre_join = 'INNER JOIN biblio.record_entry bre ON m.source = bre.id';
     }
     
-    my $rank = $rel;
-
     my $desc = 'ASC';
     $desc = 'DESC' if ($self->find_modifier('descending'));
 
     my $nullpos = 'NULLS LAST';
     $nullpos = 'NULLS FIRST' if ($self->find_modifier('nullsfirst'));
 
+    # Do we have a badges() filter?
+    my $badges = '';
+    my ($badge_filter) = $self->find_filter('badges');
+    if ($badge_filter && @{$badge_filter->args}) {
+        $badges = join (',', grep /^\d+$/, @{$badge_filter->args});
+    }
+
+    # Do we have a badge_orgs() filter? (used for calculating popularity)
+    my $borgs = '';
+    my ($bo_filter) = $self->find_filter('badge_orgs');
+    if ($bo_filter && @{$bo_filter->args}) {
+        $borgs = join (',', grep /^\d+$/, @{$bo_filter->args});
+    }
+
+    # Build the badge-ish WITH query
+    my $pop_with = <<'    WITH';
+        pop_with AS (
+            SELECT  record,
+                    ARRAY_AGG(badge) AS badges,
+                    SUM(s.score::NUMERIC*b.weight::NUMERIC)/SUM(b.weight::NUMERIC) AS total_score
+              FROM  rating.record_badge_score s
+                    JOIN rating.badge b ON (
+                        b.id = s.badge
+    WITH
+
+    $pop_with .= " AND b.id = ANY ('{$badges}')" if ($badges);
+    $pop_with .= " AND b.scope = ANY ('{$borgs}')" if ($borgs);
+    $pop_with .= ') GROUP BY 1)'; 
+
+    my $pop_join = $badges ? # inner join if we are restricting via badges()
+        'INNER JOIN pop_with ON ( m.source = pop_with.record )' : 
+        'LEFT JOIN pop_with ON ( m.source = pop_with.record )';
+
+    $$flat_plan{with} .= ',' if $$flat_plan{with};
+    $$flat_plan{with} .= $pop_with;
+
+
+    my $rank;
+    my $pop_extra_sort = '';
     if (grep {$_ eq $sort_filter} @{$self->QueryParser->dynamic_sorters}) {
         $rank = "FIRST((SELECT value FROM metabib.record_sorter rbr WHERE rbr.source = m.source and attr = '$sort_filter'))"
     } elsif ($sort_filter eq 'create_date') {
         $rank = "FIRST((SELECT create_date FROM biblio.record_entry rbr WHERE rbr.id = m.source))";
     } elsif ($sort_filter eq 'edit_date') {
         $rank = "FIRST((SELECT edit_date FROM biblio.record_entry rbr WHERE rbr.id = m.source))";
+    } elsif ($sort_filter eq 'poprel') {
+        $rank = '1.0/((' . $rel . ') * (1.0 + AVG(COALESCE(pop_with.total_score::NUMERIC,0.0)) / 5.0))::NUMERIC';
+    } elsif ($sort_filter =~ /^pop/) {
+        $rank = '1.0/(AVG(COALESCE(pop_with.total_score::NUMERIC,0.0)) + 5.0)::NUMERIC';
+        my $pop_desc = $desc eq 'ASC' ? 'DESC' : 'ASC';
+        $pop_extra_sort = "3 $pop_desc $nullpos,";
     } else {
         # default to rel ranking
-        $rank = $rel;
+        $rank = "1.0/($rel)::NUMERIC";
     }
 
     my $key = 'm.source';
@@ -877,18 +921,21 @@ SELECT  $key AS id,
         $agg_records,
         $rel AS rel,
         $rank AS rank, 
-        FIRST(pubdate_t.value) AS tie_break
+        FIRST(pubdate_t.value) AS tie_break,
+        STRING_AGG(ARRAY_TO_STRING(pop_with.badges,','),',') AS badges,
+        AVG(COALESCE(pop_with.total_score::NUMERIC,0.0))::NUMERIC(2,1) AS popularity
   FROM  metabib.metarecord_source_map m
         $$flat_plan{from}
-        $pubdate_join
         $mra_join
         $mrv_join
         $bre_join
+        $pop_join
+        $pubdate_join
         $lang_join
   WHERE 1=1
         $flat_where
   GROUP BY 1
-  ORDER BY 4 $desc $nullpos, 5 DESC $nullpos, 3 DESC
+  ORDER BY 4 $desc $nullpos, $pop_extra_sort 5 DESC $nullpos, 3 DESC
   LIMIT $core_limit
 SQL
 
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 fad6ef3..aebd348 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
@@ -3067,6 +3067,8 @@ sub query_parser_fts {
     delete $$summary_row{id};
     delete $$summary_row{rel};
     delete $$summary_row{record};
+    delete $$summary_row{badges};
+    delete $$summary_row{popularity};
 
     if (defined($simple_plan)) {
         $$summary_row{complex_query} = $simple_plan ? 0 : 1;
@@ -3098,6 +3100,8 @@ __PACKAGE__->register_method(
     cachable    => 1,
 );
 
+my $top_org;
+
 sub query_parser_fts_wrapper {
     my $self = shift;
     my $client = shift;
@@ -3114,6 +3118,8 @@ sub query_parser_fts_wrapper {
         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}} ) {
@@ -3144,7 +3150,59 @@ sub query_parser_fts_wrapper {
         if ($args{preferred_language_weight} and !$base_plan->parse_tree->find_filter('preferred_language_weight') and !$base_plan->parse_tree->find_filter('preferred_language_multiplier'));
 
 
+    my $borgs = undef;
+    if (!$base_plan->parse_tree->find_filter('badge_orgs')) {
+        # supply a suitable badge_orgs filter unless user has
+        # explicitly supplied one
+        my $site = undef;
+
+        my @lg_id_list = @{$args{location_groups}} if (ref $args{location_groups});
+
+        my ($lg_filter) = $base_plan->parse_tree->find_filter('location_groups');
+        @lg_id_list = @{$lg_filter->args} if ($lg_filter && @{$lg_filter->args});
+
+        if (@lg_id_list) {
+            my @borg_list;
+            for my $lg ( grep { /^\d+$/ } @lg_id_list ) {
+                my $lg_obj = asset::copy_location_group->retrieve($lg);
+                next unless $lg_obj;
+    
+                push(@borg_list, ''.$lg_obj->owner);
+            }
+            $borgs = join(',', @borg_list) if @borg_list;
+        }
+    
+        if (!$borgs) {
+            my ($site_filter) = $base_plan->parse_tree->find_filter('site');
+            if ($site_filter && @{$site_filter->args}) {
+                $site = $top_org if ($site_filter->args->[0] eq '-');
+                $site = $top_org if ($site_filter->args->[0] eq $top_org->shortname);
+                $site = actor::org_unit->search( { shortname => $site_filter->args->[0] })->next unless ($site);
+            } elsif ($args{org_unit}) {
+                $site = $top_org if ($args{org_unit} eq '-');
+                $site = $top_org if ($args{org_unit} eq $top_org->shortname);
+                $site = actor::org_unit->search( { shortname => $args{org_unit} })->next unless ($site);
+            } else {
+                $site = $top_org;
+            }
+
+            if ($site) {
+                $borgs = OpenSRF::AppSession->create( 'open-ils.cstore' )->request(
+                    'open-ils.cstore.json_query.atomic',
+                    { from => [ 'actor.org_unit_ancestors', $site->id ] }
+                )->gather(1);
+
+                if (ref $borgs && @$borgs) {
+                    $borgs = join(',', map { $_->{'id'} } @$borgs);
+                } else {
+                    $borgs = undef;
+                }
+            }
+        }
+    }
+
     $query = "estimation_strategy($args{estimation_strategy}) $query" if ($args{estimation_strategy});
+    $query = "badge_orgs($borgs) $query" if ($borgs);
     $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});
@@ -3173,7 +3231,7 @@ 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/ ) {
+    for my $filter ( qw/locations location_groups statuses between audience language lit_form item_form item_type bib_level vr_format badges/ ) {
         if (my $s = $args{$filter}) {
             $s = [$s] if (!ref($s));
 
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Record.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Record.pm
index 5f3d51c..0253478 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Record.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Record.pm
@@ -66,6 +66,21 @@ sub load_record {
         $self->mk_copy_query($rec_id, $org, $copy_depth, $copy_limit, $copy_offset, $pref_ou)
     );
 
+    if ($self->cgi->param('badges')) {
+        my $badges = $self->cgi->param('badges');
+        $badges = $badges ? [split(',', $badges)] : [];
+        $badges = [grep { /^\d+$/ }, @$badges];
+        if (@$badges) {
+            $self->ctx->{badge_scores} = $cstore->request(
+                'open-ils.cstore.direct.rating.record_badge_score.search.atomic',
+                { record => $rec_id, badge => $badges },
+                { flesh => 1, flesh_fields => { rrbs => ['badge'] } }
+            )->gather(1);
+        }
+    } else {
+        $self->ctx->{badge_scores} = [];
+    }
+
     # find foreign copy data
     my $peer_rec = $U->simplereq(
         'open-ils.search',
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 51bdf23..7ee9cfa 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm
@@ -522,13 +522,15 @@ sub load_rresults {
         }
     }
 
-    if ($tag_circs) {
-        for my $rec (@{$ctx->{records}}) {
-            my ($res_rec) = grep { $_->[0] == $rec->{$id_key} } @{$results->{ids}};
-            # index 1 in the per-record result array is a boolean which
+    for my $rec (@{$ctx->{records}}) {
+        my ($res_rec) = grep { $_->[0] == $rec->{$id_key} } @{$results->{ids}};
+        $rec->{badges} = [split(',', $res_rec->[1])] if $res_rec->[1];
+        $rec->{popularity} = $res_rec->[2];
+        if ($tag_circs) {
+            # index 3 (5 for MR) in the per-record result array is a boolean which
             # indicates whether the record in question is in the users
             # accessible circ history list
-            my $index = $is_meta ? 3 : 1;
+            my $index = $is_meta ? 5 : 3;
             $rec->{user_circulated} = 1 if $res_rec->[$index];
         }
     }
diff --git a/Open-ILS/src/sql/Pg/upgrade/YYYY.function.qp_search.sql b/Open-ILS/src/sql/Pg/upgrade/YYYY.function.qp_search.sql
new file mode 100644
index 0000000..9dbbfe5
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/upgrade/YYYY.function.qp_search.sql
@@ -0,0 +1,396 @@
+/*
+ * Copyright (C) 2016 Equinox Software, Inc.
+ * Mike Rylander <miker at esilibrary.com> 
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ */
+
+
+BEGIN;
+
+ALTER TYPE search.search_result ADD ATTRIBUTE badges TEXT, ADD ATTRIBUTE popularity NUMERIC;
+
+CREATE OR REPLACE FUNCTION search.query_parser_fts (
+
+    param_search_ou INT,
+    param_depth     INT,
+    param_query     TEXT,
+    param_statuses  INT[],
+    param_locations INT[],
+    param_offset    INT,
+    param_check     INT,
+    param_limit     INT,
+    metarecord      BOOL,
+    staff           BOOL,
+    deleted_search  BOOL,
+    param_pref_ou   INT DEFAULT NULL
+) RETURNS SETOF search.search_result AS $func$
+DECLARE
+
+    current_res         search.search_result%ROWTYPE;
+    search_org_list     INT[];
+    luri_org_list       INT[];
+    tmp_int_list        INT[];
+
+    check_limit         INT;
+    core_limit          INT;
+    core_offset         INT;
+    tmp_int             INT;
+
+    core_result         RECORD;
+    core_cursor         REFCURSOR;
+    core_rel_query      TEXT;
+
+    total_count         INT := 0;
+    check_count         INT := 0;
+    deleted_count       INT := 0;
+    visible_count       INT := 0;
+    excluded_count      INT := 0;
+
+    luri_as_copy        BOOL;
+BEGIN
+
+    check_limit := COALESCE( param_check, 1000 );
+    core_limit  := COALESCE( param_limit, 25000 );
+    core_offset := COALESCE( param_offset, 0 );
+
+    SELECT COALESCE( enabled, FALSE ) INTO luri_as_copy FROM config.global_flag WHERE name = 'opac.located_uri.act_as_copy';
+
+    -- core_skip_chk := COALESCE( param_skip_chk, 1 );
+
+    IF param_search_ou > 0 THEN
+        IF param_depth IS NOT NULL THEN
+            SELECT ARRAY_AGG(distinct id) INTO search_org_list FROM actor.org_unit_descendants( param_search_ou, param_depth );
+        ELSE
+            SELECT ARRAY_AGG(distinct id) INTO search_org_list FROM actor.org_unit_descendants( param_search_ou );
+        END IF;
+
+        IF luri_as_copy THEN
+            SELECT ARRAY_AGG(distinct id) INTO luri_org_list FROM actor.org_unit_full_path( param_search_ou );
+        ELSE
+            SELECT ARRAY_AGG(distinct id) INTO luri_org_list FROM actor.org_unit_ancestors( param_search_ou );
+        END IF;
+
+    ELSIF param_search_ou < 0 THEN
+        SELECT ARRAY_AGG(distinct org_unit) INTO search_org_list FROM actor.org_lasso_map WHERE lasso = -param_search_ou;
+
+        FOR tmp_int IN SELECT * FROM UNNEST(search_org_list) LOOP
+
+            IF luri_as_copy THEN
+                SELECT ARRAY_AGG(distinct id) INTO tmp_int_list FROM actor.org_unit_full_path( tmp_int );
+            ELSE
+                SELECT ARRAY_AGG(distinct id) INTO tmp_int_list FROM actor.org_unit_ancestors( tmp_int );
+            END IF;
+
+            luri_org_list := luri_org_list || tmp_int_list;
+        END LOOP;
+
+        SELECT ARRAY_AGG(DISTINCT x.id) INTO luri_org_list FROM UNNEST(luri_org_list) x(id);
+
+    ELSIF param_search_ou = 0 THEN
+        -- reserved for user lassos (ou_buckets/type='lasso') with ID passed in depth ... hack? sure.
+    END IF;
+
+    IF param_pref_ou IS NOT NULL THEN
+            IF luri_as_copy THEN
+                SELECT ARRAY_AGG(distinct id) INTO tmp_int_list FROM actor.org_unit_full_path( param_pref_ou );
+            ELSE
+                SELECT ARRAY_AGG(distinct id) INTO tmp_int_list FROM actor.org_unit_ancestors( param_pref_ou );
+            END IF;
+
+        luri_org_list := luri_org_list || tmp_int_list;
+    END IF;
+
+    OPEN core_cursor FOR EXECUTE param_query;
+
+    LOOP
+
+        FETCH core_cursor INTO core_result;
+        EXIT WHEN NOT FOUND;
+        EXIT WHEN total_count >= core_limit;
+
+        total_count := total_count + 1;
+
+        CONTINUE WHEN total_count NOT BETWEEN  core_offset + 1 AND check_limit + core_offset;
+
+        check_count := check_count + 1;
+
+        IF NOT deleted_search THEN
+
+            PERFORM 1 FROM biblio.record_entry b WHERE NOT b.deleted AND b.id IN ( SELECT * FROM unnest( core_result.records ) );
+            IF NOT FOUND THEN
+                -- RAISE NOTICE ' % were all deleted ... ', core_result.records;
+                deleted_count := deleted_count + 1;
+                CONTINUE;
+            END IF;
+
+            PERFORM 1
+              FROM  biblio.record_entry b
+                    JOIN config.bib_source s ON (b.source = s.id)
+              WHERE s.transcendant
+                    AND b.id IN ( SELECT * FROM unnest( core_result.records ) );
+
+            IF FOUND THEN
+                -- RAISE NOTICE ' % were all transcendant ... ', core_result.records;
+                visible_count := visible_count + 1;
+
+                current_res.id = core_result.id;
+                current_res.rel = core_result.rel;
+                current_res.badges = core_result.badges;
+                current_res.popularity = core_result.popularity;
+
+                tmp_int := 1;
+                IF metarecord THEN
+                    SELECT COUNT(DISTINCT s.source) INTO tmp_int FROM metabib.metarecord_source_map s WHERE s.metarecord = core_result.id;
+                END IF;
+
+                IF tmp_int = 1 THEN
+                    current_res.record = core_result.records[1];
+                ELSE
+                    current_res.record = NULL;
+                END IF;
+
+                RETURN NEXT current_res;
+
+                CONTINUE;
+            END IF;
+
+            PERFORM 1
+              FROM  asset.call_number cn
+                    JOIN asset.uri_call_number_map map ON (map.call_number = cn.id)
+                    JOIN asset.uri uri ON (map.uri = uri.id)
+              WHERE NOT cn.deleted
+                    AND cn.label = '##URI##'
+                    AND uri.active
+                    AND ( param_locations IS NULL OR array_upper(param_locations, 1) IS NULL )
+                    AND cn.record IN ( SELECT * FROM unnest( core_result.records ) )
+                    AND cn.owning_lib IN ( SELECT * FROM unnest( luri_org_list ) )
+              LIMIT 1;
+
+            IF FOUND THEN
+                -- RAISE NOTICE ' % have at least one URI ... ', core_result.records;
+                visible_count := visible_count + 1;
+
+                current_res.id = core_result.id;
+                current_res.rel = core_result.rel;
+                current_res.badges = core_result.badges;
+                current_res.popularity = core_result.popularity;
+
+                tmp_int := 1;
+                IF metarecord THEN
+                    SELECT COUNT(DISTINCT s.source) INTO tmp_int FROM metabib.metarecord_source_map s WHERE s.metarecord = core_result.id;
+                END IF;
+
+                IF tmp_int = 1 THEN
+                    current_res.record = core_result.records[1];
+                ELSE
+                    current_res.record = NULL;
+                END IF;
+
+                RETURN NEXT current_res;
+
+                CONTINUE;
+            END IF;
+
+            IF param_statuses IS NOT NULL AND array_upper(param_statuses, 1) > 0 THEN
+
+                PERFORM 1
+                  FROM  asset.call_number cn
+                        JOIN asset.copy cp ON (cp.call_number = cn.id)
+                  WHERE NOT cn.deleted
+                        AND NOT cp.deleted
+                        AND cp.status IN ( SELECT * FROM unnest( param_statuses ) )
+                        AND cn.record IN ( SELECT * FROM unnest( core_result.records ) )
+                        AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
+                  LIMIT 1;
+
+                IF NOT FOUND THEN
+                    PERFORM 1
+                      FROM  biblio.peer_bib_copy_map pr
+                            JOIN asset.copy cp ON (cp.id = pr.target_copy)
+                      WHERE NOT cp.deleted
+                            AND cp.status IN ( SELECT * FROM unnest( param_statuses ) )
+                            AND pr.peer_record IN ( SELECT * FROM unnest( core_result.records ) )
+                            AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
+                      LIMIT 1;
+
+                    IF NOT FOUND THEN
+                    -- RAISE NOTICE ' % and multi-home linked records were all status-excluded ... ', core_result.records;
+                        excluded_count := excluded_count + 1;
+                        CONTINUE;
+                    END IF;
+                END IF;
+
+            END IF;
+
+            IF param_locations IS NOT NULL AND array_upper(param_locations, 1) > 0 THEN
+
+                PERFORM 1
+                  FROM  asset.call_number cn
+                        JOIN asset.copy cp ON (cp.call_number = cn.id)
+                  WHERE NOT cn.deleted
+                        AND NOT cp.deleted
+                        AND cp.location IN ( SELECT * FROM unnest( param_locations ) )
+                        AND cn.record IN ( SELECT * FROM unnest( core_result.records ) )
+                        AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
+                  LIMIT 1;
+
+                IF NOT FOUND THEN
+                    PERFORM 1
+                      FROM  biblio.peer_bib_copy_map pr
+                            JOIN asset.copy cp ON (cp.id = pr.target_copy)
+                      WHERE NOT cp.deleted
+                            AND cp.location IN ( SELECT * FROM unnest( param_locations ) )
+                            AND pr.peer_record IN ( SELECT * FROM unnest( core_result.records ) )
+                            AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
+                      LIMIT 1;
+
+                    IF NOT FOUND THEN
+                        -- RAISE NOTICE ' % and multi-home linked records were all copy_location-excluded ... ', core_result.records;
+                        excluded_count := excluded_count + 1;
+                        CONTINUE;
+                    END IF;
+                END IF;
+
+            END IF;
+
+            IF staff IS NULL OR NOT staff THEN
+
+                PERFORM 1
+                  FROM  asset.opac_visible_copies
+                  WHERE circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
+                        AND record IN ( SELECT * FROM unnest( core_result.records ) )
+                  LIMIT 1;
+
+                IF NOT FOUND THEN
+                    PERFORM 1
+                      FROM  biblio.peer_bib_copy_map pr
+                            JOIN asset.opac_visible_copies cp ON (cp.copy_id = pr.target_copy)
+                      WHERE cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
+                            AND pr.peer_record IN ( SELECT * FROM unnest( core_result.records ) )
+                      LIMIT 1;
+
+                    IF NOT FOUND THEN
+
+                        -- RAISE NOTICE ' % and multi-home linked records were all visibility-excluded ... ', core_result.records;
+                        excluded_count := excluded_count + 1;
+                        CONTINUE;
+                    END IF;
+                END IF;
+
+            ELSE
+
+                PERFORM 1
+                  FROM  asset.call_number cn
+                        JOIN asset.copy cp ON (cp.call_number = cn.id)
+                  WHERE NOT cn.deleted
+                        AND NOT cp.deleted
+                        AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
+                        AND cn.record IN ( SELECT * FROM unnest( core_result.records ) )
+                  LIMIT 1;
+
+                IF NOT FOUND THEN
+
+                    PERFORM 1
+                      FROM  biblio.peer_bib_copy_map pr
+                            JOIN asset.copy cp ON (cp.id = pr.target_copy)
+                      WHERE NOT cp.deleted
+                            AND cp.circ_lib IN ( SELECT * FROM unnest( search_org_list ) )
+                            AND pr.peer_record IN ( SELECT * FROM unnest( core_result.records ) )
+                      LIMIT 1;
+
+                    IF NOT FOUND THEN
+
+                        PERFORM 1
+                          FROM  asset.call_number cn
+                                JOIN asset.copy cp ON (cp.call_number = cn.id)
+                          WHERE cn.record IN ( SELECT * FROM unnest( core_result.records ) )
+                                AND NOT cp.deleted
+                          LIMIT 1;
+
+                        IF NOT FOUND THEN
+                            -- Recheck Located URI visibility in the case of no "foreign" copies
+                            PERFORM 1
+                              FROM  asset.call_number cn
+                                    JOIN asset.uri_call_number_map map ON (map.call_number = cn.id)
+                                    JOIN asset.uri uri ON (map.uri = uri.id)
+                              WHERE NOT cn.deleted
+                                    AND cn.label = '##URI##'
+                                    AND uri.active
+                                    AND cn.record IN ( SELECT * FROM unnest( core_result.records ) )
+                                    AND cn.owning_lib NOT IN ( SELECT * FROM unnest( luri_org_list ) )
+                              LIMIT 1;
+
+                            IF FOUND THEN
+                                -- RAISE NOTICE ' % were excluded for foreign located URIs... ', core_result.records;
+                                excluded_count := excluded_count + 1;
+                                CONTINUE;
+                            END IF;
+                        ELSE
+                            -- RAISE NOTICE ' % and multi-home linked records were all visibility-excluded ... ', core_result.records;
+                            excluded_count := excluded_count + 1;
+                            CONTINUE;
+                        END IF;
+                    END IF;
+
+                END IF;
+
+            END IF;
+
+        END IF;
+
+        visible_count := visible_count + 1;
+
+        current_res.id = core_result.id;
+        current_res.rel = core_result.rel;
+        current_res.badges = core_result.badges;
+        current_res.popularity = core_result.popularity;
+
+        tmp_int := 1;
+        IF metarecord THEN
+            SELECT COUNT(DISTINCT s.source) INTO tmp_int FROM metabib.metarecord_source_map s WHERE s.metarecord = core_result.id;
+        END IF;
+
+        IF tmp_int = 1 THEN
+            current_res.record = core_result.records[1];
+        ELSE
+            current_res.record = NULL;
+        END IF;
+
+        RETURN NEXT current_res;
+
+        IF visible_count % 1000 = 0 THEN
+            -- RAISE NOTICE ' % visible so far ... ', visible_count;
+        END IF;
+
+    END LOOP;
+
+    current_res.id = NULL;
+    current_res.rel = NULL;
+    current_res.record = NULL;
+    current_res.badges = NULL;
+    current_res.popularity = NULL;
+    current_res.total = total_count;
+    current_res.checked = check_count;
+    current_res.deleted = deleted_count;
+    current_res.visible = visible_count;
+    current_res.excluded = excluded_count;
+
+    CLOSE core_cursor;
+
+    RETURN NEXT current_res;
+
+END;
+$func$ LANGUAGE PLPGSQL;
+ 
+COMMIT;
+
diff --git a/Open-ILS/src/templates/opac/parts/record/summary.tt2 b/Open-ILS/src/templates/opac/parts/record/summary.tt2
index 76f45ff..b2bd5e5 100644
--- a/Open-ILS/src/templates/opac/parts/record/summary.tt2
+++ b/Open-ILS/src/templates/opac/parts/record/summary.tt2
@@ -314,6 +314,16 @@ IF num_uris > 0;
         </span>
     </li>
     [%- END %]
+    [%- IF (ctx.badge_scores.size > 0) %]
+    <li id='rdetail_badges'>
+        <strong class='rdetail_label'>[% l("Badges:") %]</strong>
+        <ul>
+          [% FOR bscore IN ctx.badge_scores; %]
+            <li><strong>[% bscore.badge.name | html %]</strong>: [% bscore.score %] / 5.0</li>
+        [%- END -%]
+        </span>
+    </li>
+    [%- END %]
 </ul>
 
 [%- INCLUDE "opac/parts/record/contents.tt2" %]
diff --git a/Open-ILS/src/templates/opac/parts/result/table.tt2 b/Open-ILS/src/templates/opac/parts/result/table.tt2
index 960d8c6..02fe684 100644
--- a/Open-ILS/src/templates/opac/parts/result/table.tt2
+++ b/Open-ILS/src/templates/opac/parts/result/table.tt2
@@ -56,11 +56,11 @@
                                 ELSE;
                                     # for MR, bre_id refers to the master and in
                                     # this case, only, record
-                                    record_url = mkurl(ctx.opac_root _ '/record/' _ rec.bre_id);
+                                    record_url = mkurl(ctx.opac_root _ '/record/' _ rec.bre_id, { badges => rec.badges.join(',') });
                                 END;
                                 hold_type = 'M';
                             ELSE;
-                                record_url = mkurl(ctx.opac_root _ '/record/' _ rec.bre_id);
+                                record_url = mkurl(ctx.opac_root _ '/record/' _ rec.bre_id, { badges => rec.badges.join(',') });
                                 hold_type = 'T';
                             END;
                     -%]
@@ -155,6 +155,11 @@ END;
                                                         END
                                                     -%]
                                                     </div>
+                                                    [% IF rec.popularity > 0.0 %]
+                                                      <div>
+                                                        <span><strong>[% l('Popularity:') %]</strong> [% rec.popularity %] / 5.0</span>
+                                                      </div>
+                                                    [% END %]
                                                     <table 
                                                        role="presentation"
                                                        title="[% l('Record Holdings Summary') %]"

commit 1dd8d953b01ccedc5e42162d53471f132156e77a
Author: Mike Rylander <mrylander at gmail.com>
Date:   Thu Jan 7 22:00:20 2016 -0500

    LP#1549505: schema and IDL for statistical poularity ratings
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at esilibrary.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml
index 7bca4f2..6f86b51 100644
--- a/Open-ILS/examples/fm_IDL.xml
+++ b/Open-ILS/examples/fm_IDL.xml
@@ -240,6 +240,98 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 		</permacrud>
 	</class>
 
+	<class  id="rrbs" 
+            controller="open-ils.cstore open-ils.pcrud" 
+            oils_obj:fieldmapper="rating::record_badge_score" 
+            oils_persist:tablename="rating.record_badge_score" 
+            reporter:label="Statistical Popularity Badge">
+		<fields oils_persist:primary="id" 
+                oils_persist:sequence="rating.record_badge_score_id_seq">
+			<field reporter:label="ID" name="id" reporter:datatype="id" reporter:selector="name"/>
+			<field reporter:label="Badge" name="badge" reporter:datatype="link"/>
+			<field reporter:label="Record" name="record" reporter:datatype="link"/>
+			<field reporter:label="Score" name="score" reporter:datatype="int"/>
+		</fields>
+        <links>
+            <link field="badge" reltype="has_a" key="id" map="" class="rb"/>
+            <link field="record" reltype="has_a" key="id" map="" class="bre"/>
+        </links>
+		<permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+			<actions>
+				<retrieve/>
+			</actions>
+		</permacrud>
+	</class>
+
+	<class  id="rp" 
+            controller="open-ils.cstore open-ils.pcrud" 
+            oils_obj:fieldmapper="rating::popularity_parameter" 
+            oils_persist:tablename="rating.popularity_parameter" 
+            reporter:label="Statistical Popularity Parameter">
+		<fields oils_persist:primary="id" 
+                oils_persist:sequence="rating.popularity_parameter_id_seq">
+			<field reporter:label="ID" name="id" reporter:datatype="id" reporter:selector="name"/>
+			<field reporter:label="Name" name="name" reporter:datatype="text"/>
+			<field reporter:label="Description" name="description" reporter:datatype="text"/>
+			<field reporter:label="Population Function" name="func" reporter:datatype="text"/>
+			<field reporter:label="Require Horizon" name="require_horizon" reporter:datatype="bool"/>
+			<field reporter:label="Require Percentile" name="require_percentile" reporter:datatype="bool"/>
+			<field reporter:label="Require Importance" name="require_importance" reporter:datatype="bool"/>
+		</fields>
+		<permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+			<actions>
+				<create permission="CREATE_POP_PARAMETER" global_required="true"/>
+				<retrieve/>
+				<update permission="UPDATE_POP_PARAMETER" global_required="true"/>
+				<delete permission="DELETE_POP_PARAMETER" global_required="true"/>
+			</actions>
+		</permacrud>
+	</class>
+
+	<class  id="rb" 
+            controller="open-ils.cstore open-ils.pcrud" 
+            oils_obj:fieldmapper="rating::badge" 
+            oils_persist:tablename="rating.badge" 
+            reporter:label="Statistical Popularity Badge">
+		<fields oils_persist:primary="id" 
+                oils_persist:sequence="rating.badge_id_seq">
+			<field reporter:label="ID" name="id" reporter:datatype="id" reporter:selector="name"/>
+			<field reporter:label="Name" name="name" reporter:datatype="text" oils_persist:i18n="true"/>
+			<field reporter:label="Description" name="description" reporter:datatype="text" oils_persist:i18n="true"/>
+			<field reporter:label="Scope" name="scope" reporter:datatype="org_unit"/>
+			<field reporter:label="Weight" name="weight" reporter:datatype="int"/>
+			<field reporter:label="Age Horizon" name="horizon_age" reporter:datatype="text"/>
+			<field reporter:label="Importance Horizon" name="importance_age" reporter:datatype="text"/>
+			<field reporter:label="Importance Interval" name="importance_interval" reporter:datatype="text"/>
+			<field reporter:label="Importance Scale" name="importance_scale" reporter:datatype="text"/>
+			<field reporter:label="Percentile" name="percentile" reporter:datatype="int"/>
+			<field reporter:label="Attribute Filter" name="attr_filter" reporter:datatype="text"/>
+			<field reporter:label="Circ Mod Filter" name="circ_mod_filter" reporter:datatype="link"/>
+			<field reporter:label="Bib Source Filter" name="src_filter" reporter:datatype="link"/>
+			<field reporter:label="Location Group Filter" name="loc_grp_filter" reporter:datatype="link"/>
+			<field reporter:label="Recalculation Interval" name="recalc_interval" reporter:datatype="text"/>
+			<field reporter:label="Fixed Rating" name="fixed_rating" reporter:datatype="int"/>
+			<field reporter:label="Discard Value Count" name="discard" reporter:datatype="int"/>
+			<field reporter:label="Last Refresh Time" name="last_calc" reporter:datatype="timestamp"/>
+			<field reporter:label="Popularity Parameter" name="popularity_parameter" reporter:datatype="link"/>
+		</fields>
+		<links>
+			<link field="scope" reltype="has_a" key="id" map="" class="aou"/>
+			<link field="popularity_parameter" reltype="has_a" key="id" map="" class="rp"/>
+			<link field="src_filter" reltype="has_a" key="id" map="" class="cbs"/>
+			<link field="circ_mod_filter" reltype="has_a" key="code" map="" class="ccm"/>
+			<link field="loc_grp_filter" reltype="has_a" key="id" map="" class="acplg"/>
+		</links>
+		<permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+			<actions>
+				<create permission="CREATE_POP_BADGE" global_required="true"/>
+				<retrieve/>
+				<update permission="UPDATE_POP_BADGE" global_required="true"/>
+				<delete permission="DELETE_POP_BADGE" global_required="true"/>
+			</actions>
+		</permacrud>
+	</class>
+
 	<class  id="vibtg" 
             controller="open-ils.cstore open-ils.pcrud" 
             oils_obj:fieldmapper="vandelay::import_bib_trash_group" 
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.statisitcal-ratings.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.statisitcal-ratings.sql
new file mode 100644
index 0000000..1c9324d
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.statisitcal-ratings.sql
@@ -0,0 +1,984 @@
+
+BEGIN;
+
+-- Create these so that the queries in the UDFs will validate
+CREATE TEMP TABLE precalc_filter_bib_list (
+    id  BIGINT
+) ON COMMIT DROP;
+
+CREATE TEMP TABLE precalc_bib_filter_bib_list (
+    id  BIGINT
+) ON COMMIT DROP;
+
+CREATE TEMP TABLE precalc_src_filter_bib_list (
+    id  BIGINT
+) ON COMMIT DROP;
+
+CREATE TEMP TABLE precalc_copy_filter_bib_list (
+    id  BIGINT,
+    copy  BIGINT
+) ON COMMIT DROP;
+
+CREATE TEMP TABLE precalc_circ_mod_filter_bib_list (
+    id  BIGINT,
+    copy  BIGINT
+) ON COMMIT DROP;
+
+CREATE TEMP TABLE precalc_location_filter_bib_list (
+    id  BIGINT,
+    copy  BIGINT
+) ON COMMIT DROP;
+
+CREATE TEMP TABLE precalc_attr_filter_bib_list (
+    id  BIGINT
+) ON COMMIT DROP;
+
+CREATE TEMP TABLE precalc_bibs_by_copy_list (
+    id  BIGINT
+) ON COMMIT DROP;
+
+CREATE TEMP TABLE precalc_bibs_by_uri_list (
+    id  BIGINT
+) ON COMMIT DROP;
+
+CREATE TEMP TABLE precalc_bibs_by_copy_or_uri_list (
+    id  BIGINT
+) ON COMMIT DROP;
+
+CREATE TEMP TABLE precalc_bib_list (
+    id  BIGINT
+) ON COMMIT DROP;
+
+-- rhrr needs to be a real table, so it can be fast. To that end, we use
+-- a materialized view updated via a trigger.
+
+DROP VIEW reporter.hold_request_record;
+
+CREATE TABLE reporter.hold_request_record  AS
+SELECT  id,
+        target,
+        hold_type,
+        CASE
+                WHEN hold_type = 'T'
+                        THEN target
+                WHEN hold_type = 'I'
+                        THEN (SELECT ssub.record_entry FROM serial.subscription ssub JOIN serial.issuance si ON (si.subscription = ssub.id) WHERE si.id = ahr.target)
+                WHEN hold_type = 'V'
+                        THEN (SELECT cn.record FROM asset.call_number cn WHERE cn.id = ahr.target)
+                WHEN hold_type IN ('C','R','F')
+                        THEN (SELECT cn.record FROM asset.call_number cn JOIN asset.copy cp ON (cn.id = cp.call_number) WHERE cp.id = ahr.target)
+                WHEN hold_type = 'M'
+                        THEN (SELECT mr.master_record FROM metabib.metarecord mr WHERE mr.id = ahr.target)
+                WHEN hold_type = 'P'
+                        THEN (SELECT bmp.record FROM biblio.monograph_part bmp WHERE bmp.id = ahr.target)
+        END AS bib_record
+  FROM  action.hold_request ahr;
+
+CREATE UNIQUE INDEX reporter_hold_request_record_pkey_idx ON reporter.hold_request_record (id);
+CREATE INDEX reporter_hold_request_record_bib_record_idx ON reporter.hold_request_record (bib_record);
+
+ALTER TABLE reporter.hold_request_record ADD PRIMARY KEY USING INDEX reporter_hold_request_record_pkey_idx;
+
+CREATE FUNCTION reporter.hold_request_record_mapper () RETURNS TRIGGER AS $$
+BEGIN
+    IF TG_OP = 'INSERT' THEN
+        INSERT INTO reporter.hold_request_record (id, target, hold_type, bib_record)
+        SELECT  NEW.id,
+                NEW.target,
+                NEW.hold_type,
+                CASE
+                    WHEN NEW.hold_type = 'T'
+                        THEN NEW.target
+                    WHEN NEW.hold_type = 'I'
+                        THEN (SELECT ssub.record_entry FROM serial.subscription ssub JOIN serial.issuance si ON (si.subscription = ssub.id) WHERE si.id = NEW.target)
+                    WHEN NEW.hold_type = 'V'
+                        THEN (SELECT cn.record FROM asset.call_number cn WHERE cn.id = NEW.target)
+                    WHEN NEW.hold_type IN ('C','R','F')
+                        THEN (SELECT cn.record FROM asset.call_number cn JOIN asset.copy cp ON (cn.id = cp.call_number) WHERE cp.id = NEW.target)
+                    WHEN NEW.hold_type = 'M'
+                        THEN (SELECT mr.master_record FROM metabib.metarecord mr WHERE mr.id = NEW.target)
+                    WHEN NEW.hold_type = 'P'
+                        THEN (SELECT bmp.record FROM biblio.monograph_part bmp WHERE bmp.id = NEW.target)
+                END AS bib_record;
+    ELSIF TG_OP = 'UPDATE' AND (OLD.target <> NEW.target OR OLD.hold_type <> NEW.hold_type) THEN
+        UPDATE  reporter.hold_request_record
+          SET   target = NEW.target,
+                hold_type = NEW.hold_type,
+                bib_record = CASE
+                    WHEN NEW.hold_type = 'T'
+                        THEN NEW.target
+                    WHEN NEW.hold_type = 'I'
+                        THEN (SELECT ssub.record_entry FROM serial.subscription ssub JOIN serial.issuance si ON (si.subscription = ssub.id) WHERE si.id = NEW.target)
+                    WHEN NEW.hold_type = 'V'
+                        THEN (SELECT cn.record FROM asset.call_number cn WHERE cn.id = NEW.target)
+                    WHEN NEW.hold_type IN ('C','R','F')
+                        THEN (SELECT cn.record FROM asset.call_number cn JOIN asset.copy cp ON (cn.id = cp.call_number) WHERE cp.id = NEW.target)
+                    WHEN NEW.hold_type = 'M'
+                        THEN (SELECT mr.master_record FROM metabib.metarecord mr WHERE mr.id = NEW.target)
+                    WHEN NEW.hold_type = 'P'
+                        THEN (SELECT bmp.record FROM biblio.monograph_part bmp WHERE bmp.id = NEW.target)
+                END;
+    END IF;
+    RETURN NEW;
+END;
+$$ LANGUAGE PLPGSQL;
+
+CREATE TRIGGER reporter_hold_request_record_trigger AFTER INSERT OR UPDATE ON action.hold_request
+    FOR EACH ROW EXECUTE PROCEDURE reporter.hold_request_record_mapper();
+
+CREATE SCHEMA rating;
+
+CREATE TABLE rating.popularity_parameter (
+    id          INT     PRIMARY KEY,
+    name        TEXT    NOT NULL UNIQUE, -- i18n
+    description TEXT,
+    func        TEXT,
+    require_horizon     BOOL    NOT NULL DEFAULT FALSE,
+    require_importance  BOOL    NOT NULL DEFAULT FALSE,
+    require_percentile  BOOL    NOT NULL DEFAULT FALSE
+);
+
+INSERT INTO rating.popularity_parameter (id,name,func,require_horizon,require_importance,require_percentile) VALUES
+    (1,'Holds Filled Over Time','rating.holds_filled_over_time',TRUE,FALSE,TRUE),
+    (2,'Holds Requested Over Time','rating.holds_placed_over_time',TRUE,FALSE,TRUE),
+    (3,'Current Hold Count','rating.current_hold_count',FALSE,FALSE,TRUE),
+    (4,'Circulations Over Time','rating.circs_over_time',TRUE,FALSE,TRUE),
+    (5,'Current Circulation Count','rating.current_circ_count',FALSE,FALSE,TRUE),
+    (6,'Out/Total Ratio','rating.checked_out_total_ratio',FALSE,FALSE,TRUE),
+    (7,'Holds/Total Ratio','rating.holds_total_ratio',FALSE,FALSE,TRUE),
+    (8,'Holds/Holdable Ratio','rating.holds_holdable_ratio',FALSE,FALSE,TRUE),
+    (9,'Percent of Time Circulating','rating.percent_time_circulating',FALSE,FALSE,TRUE),
+    (10,'Bibliographic Record Age (days, newer is better)','rating.bib_record_age',FALSE,FALSE,TRUE),
+    (11,'Publication Age (days, newer is better)','rating.bib_pub_age',FALSE,FALSE,TRUE),
+    (12,'On-line Bib has attributes','rating.generic_fixed_rating_by_uri',FALSE,FALSE,FALSE),
+    (13,'Bib has attributes and copies','rating.generic_fixed_rating_by_copy',FALSE,FALSE,FALSE),
+    (14,'Bib has attributes and copies or URIs','rating.generic_fixed_rating_by_copy_or_uri',FALSE,FALSE,FALSE),
+    (15,'Bib has attributes','rating.generic_fixed_rating_global',FALSE,FALSE,FALSE);
+
+CREATE TABLE rating.badge (
+    id                      SERIAL      PRIMARY KEY,
+    name                    TEXT        NOT NULL,
+    description             TEXT,
+    scope                   INT         NOT NULL REFERENCES actor.org_unit (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    weight                  INT         NOT NULL DEFAULT 1,
+    horizon_age             INTERVAL,
+    importance_age          INTERVAL,
+    importance_interval     INTERVAL    NOT NULL DEFAULT '1 day',
+    importance_scale        NUMERIC     CHECK (importance_scale IS NULL OR importance_scale > 0.0),
+    recalc_interval         INTERVAL    NOT NULL DEFAULT '1 month',
+    attr_filter             TEXT,
+    src_filter              INT         REFERENCES config.bib_source (id) ON UPDATE CASCADE ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
+    circ_mod_filter         TEXT        REFERENCES config.circ_modifier (code) ON UPDATE CASCADE ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
+    loc_grp_filter          INT         REFERENCES asset.copy_location_group (id) ON UPDATE CASCADE ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
+    popularity_parameter    INT         NOT NULL REFERENCES rating.popularity_parameter (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    fixed_rating            INT         CHECK (fixed_rating IS NULL OR fixed_rating BETWEEN -5 AND 5),
+    percentile              NUMERIC     CHECK (percentile IS NULL OR (percentile >= 50.0 AND percentile < 100.0)),
+    discard                 INT         NOT NULL DEFAULT 0, 
+    last_calc               TIMESTAMPTZ,
+    CONSTRAINT unique_name_scope UNIQUE (name,scope)
+);
+
+CREATE TABLE rating.record_badge_score (
+    id          BIGSERIAL   PRIMARY KEY,
+    record      BIGINT      NOT NULL REFERENCES biblio.record_entry (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    badge       INT         NOT NULL REFERENCES rating.badge (id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+    score       INT         NOT NULL CHECK (score BETWEEN -5 AND 5),
+    CONSTRAINT unique_record_badge UNIQUE (record,badge)
+);
+CREATE INDEX record_badge_score_badge_idx ON rating.record_badge_score (badge);
+CREATE INDEX record_badge_score_record_idx ON rating.record_badge_score (record);
+
+CREATE OR REPLACE VIEW rating.badge_with_orgs AS
+    WITH    org_scope AS (
+                SELECT  id,
+                        array_agg(tree) AS orgs
+                  FROM  (SELECT id,
+                                (actor.org_unit_descendants(id)).id AS tree
+                          FROM  actor.org_unit
+                        ) x
+                  GROUP BY 1
+            )
+    SELECT  b.*,
+            s.orgs
+      FROM  rating.badge b
+            JOIN org_scope s ON (b.scope = s.id);
+
+CREATE OR REPLACE FUNCTION rating.precalc_src_filter(src INT)
+    RETURNS INT AS $f$
+DECLARE
+    cnt     INT     := 0;
+BEGIN
+
+    SET LOCAL client_min_messages = error;
+    DROP TABLE IF EXISTS precalc_src_filter_bib_list;
+    IF src IS NOT NULL THEN
+        CREATE TEMP TABLE precalc_src_filter_bib_list ON COMMIT DROP AS
+            SELECT id FROM biblio.record_entry
+            WHERE source = src AND NOT deleted;
+    ELSE
+        CREATE TEMP TABLE precalc_src_filter_bib_list ON COMMIT DROP AS
+            SELECT id FROM biblio.record_entry
+            WHERE id > 0 AND NOT deleted;
+    END IF;
+
+    SELECT count(*) INTO cnt FROM precalc_src_filter_bib_list;
+    RETURN cnt;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION rating.precalc_circ_mod_filter(cm TEXT)
+    RETURNS INT AS $f$
+DECLARE
+    cnt     INT     := 0;
+BEGIN
+
+    SET LOCAL client_min_messages = error;
+    DROP TABLE IF EXISTS precalc_circ_mod_filter_bib_list;
+    IF cm IS NOT NULL THEN
+        CREATE TEMP TABLE precalc_circ_mod_filter_bib_list ON COMMIT DROP AS
+            SELECT  cn.record AS id,
+                    cp.id AS copy
+              FROM  asset.call_number cn
+                    JOIN asset.copy cp ON (cn.id = cp.call_number)
+              WHERE cp.circ_modifier = cm
+                    AND NOT cp.deleted;
+    ELSE
+        CREATE TEMP TABLE precalc_circ_mod_filter_bib_list ON COMMIT DROP AS
+            SELECT  cn.record AS id,
+                    cp.id AS copy
+              FROM  asset.call_number cn
+                    JOIN asset.copy cp ON (cn.id = cp.call_number)
+              WHERE NOT cp.deleted;
+    END IF;
+
+    SELECT count(*) INTO cnt FROM precalc_circ_mod_filter_bib_list;
+    RETURN cnt;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION rating.precalc_location_filter(loc INT)
+    RETURNS INT AS $f$
+DECLARE
+    cnt     INT     := 0;
+BEGIN
+
+    SET LOCAL client_min_messages = error;
+    DROP TABLE IF EXISTS precalc_location_filter_bib_list;
+    IF loc IS NOT NULL THEN
+        CREATE TEMP TABLE precalc_location_filter_bib_list ON COMMIT DROP AS
+            SELECT  cn.record AS id,
+                    cp.id AS copy
+              FROM  asset.call_number cn
+                    JOIN asset.copy cp ON (cn.id = cp.call_number)
+                    JOIN asset.copy_location_group_map lg ON (cp.location = lg.location)
+              WHERE lg.lgroup = loc
+                    AND NOT cp.deleted;
+    ELSE
+        CREATE TEMP TABLE precalc_location_filter_bib_list ON COMMIT DROP AS
+            SELECT  cn.record AS id,
+                    cp.id AS copy
+              FROM  asset.call_number cn
+                    JOIN asset.copy cp ON (cn.id = cp.call_number)
+              WHERE NOT cp.deleted;
+    END IF;
+
+    SELECT count(*) INTO cnt FROM precalc_location_filter_bib_list;
+    RETURN cnt;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+-- all or limited...
+CREATE OR REPLACE FUNCTION rating.precalc_attr_filter(attr_filter TEXT)
+    RETURNS INT AS $f$
+DECLARE
+    cnt     INT := 0;
+    afilter TEXT;
+BEGIN
+
+    SET LOCAL client_min_messages = error;
+    DROP TABLE IF EXISTS precalc_attr_filter_bib_list;
+    IF attr_filter IS NOT NULL THEN
+        afilter := metabib.compile_composite_attr(attr_filter);
+        CREATE TEMP TABLE precalc_attr_filter_bib_list ON COMMIT DROP AS
+            SELECT source AS id FROM metabib.record_attr_vector_list
+            WHERE vlist @@ metabib.compile_composite_attr(attr_filter);
+    ELSE
+        CREATE TEMP TABLE precalc_attr_filter_bib_list ON COMMIT DROP AS
+            SELECT source AS id FROM metabib.record_attr_vector_list;
+    END IF;
+
+    SELECT count(*) INTO cnt FROM precalc_attr_filter_bib_list;
+    RETURN cnt;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION rating.precalc_bibs_by_copy(badge_id INT)
+    RETURNS INT AS $f$
+DECLARE
+    cnt         INT     := 0;
+    badge_row   rating.badge_with_orgs%ROWTYPE;
+    base        TEXT;
+    whr         TEXT;
+BEGIN
+
+    SELECT * INTO badge_row FROM rating.badge_with_orgs WHERE id = badge_id;
+
+    SET LOCAL client_min_messages = error;
+    DROP TABLE IF EXISTS precalc_bibs_by_copy_list;
+    CREATE TEMP TABLE precalc_bibs_by_copy_list ON COMMIT DROP AS
+        SELECT  DISTINCT cn.record AS id
+          FROM  asset.call_number cn
+                JOIN asset.copy cp ON (cp.call_number = cn.id AND NOT cp.deleted)
+                JOIN precalc_copy_filter_bib_list f ON (cp.id = f.copy)
+          WHERE cn.owning_lib = ANY (badge_row.orgs)
+                AND NOT cn.deleted;
+
+    SELECT count(*) INTO cnt FROM precalc_bibs_by_copy_list;
+    RETURN cnt;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION rating.precalc_bibs_by_uri(badge_id INT)
+    RETURNS INT AS $f$
+DECLARE
+    cnt         INT     := 0;
+    badge_row   rating.badge_with_orgs%ROWTYPE;
+BEGIN
+
+    SELECT * INTO badge_row FROM rating.badge_with_orgs WHERE id = badge_id;
+
+    SET LOCAL client_min_messages = error;
+    DROP TABLE IF EXISTS precalc_bibs_by_uri_list;
+    CREATE TEMP TABLE precalc_bibs_by_uri_list ON COMMIT DROP AS
+        SELECT  DISTINCT record AS id
+          FROM  asset.call_number cn
+                JOIN asset.uri_call_number_map urim ON (urim.call_number = cn.id)
+                JOIN asset.uri uri ON (urim.uri = uri.id AND uri.active)
+          WHERE cn.owning_lib = ANY (badge_row.orgs)
+                AND cn.label = '##URI##'
+                AND NOT cn.deleted;
+
+    SELECT count(*) INTO cnt FROM precalc_bibs_by_uri_list;
+    RETURN cnt;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION rating.precalc_bibs_by_copy_or_uri(badge_id INT)
+    RETURNS INT AS $f$
+DECLARE
+    cnt         INT     := 0;
+BEGIN
+
+    PERFORM rating.precalc_bibs_by_copy(badge_id);
+    PERFORM rating.precalc_bibs_by_uri(badge_id);
+
+    SET LOCAL client_min_messages = error;
+    DROP TABLE IF EXISTS precalc_bibs_by_copy_or_uri_list;
+    CREATE TEMP TABLE precalc_bibs_by_copy_or_uri_list ON COMMIT DROP AS
+        SELECT id FROM precalc_bibs_by_copy_list
+            UNION
+        SELECT id FROM precalc_bibs_by_uri_list;
+
+    SELECT count(*) INTO cnt FROM precalc_bibs_by_copy_or_uri_list;
+    RETURN cnt;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+
+CREATE OR REPLACE FUNCTION rating.recalculate_badge_score ( badge_id INT, setup_only BOOL DEFAULT FALSE ) RETURNS VOID AS $f$
+DECLARE
+    badge_row           rating.badge%ROWTYPE;
+    param           rating.popularity_parameter%ROWTYPE;
+BEGIN
+    SET LOCAL client_min_messages = error;
+
+    -- Find what we're doing    
+    SELECT * INTO badge_row FROM rating.badge WHERE id = badge_id;
+    SELECT * INTO param FROM rating.popularity_parameter WHERE id = badge_row.popularity_parameter;
+
+    -- Calculate the filtered bib set, or all bibs if none
+    PERFORM rating.precalc_attr_filter(badge_row.attr_filter);
+    PERFORM rating.precalc_src_filter(badge_row.src_filter);
+    PERFORM rating.precalc_circ_mod_filter(badge_row.circ_mod_filter);
+    PERFORM rating.precalc_location_filter(badge_row.loc_grp_filter);
+
+    -- Bring the bib-level filter lists together
+    DROP TABLE IF EXISTS precalc_bib_filter_bib_list;
+    CREATE TEMP TABLE precalc_bib_filter_bib_list ON COMMIT DROP AS
+        SELECT id FROM precalc_attr_filter_bib_list
+            INTERSECT
+        SELECT id FROM precalc_src_filter_bib_list;
+
+    -- Bring the copy-level filter lists together. We're keeping this for bib_by_copy filtering later.
+    DROP TABLE IF EXISTS precalc_copy_filter_bib_list;
+    CREATE TEMP TABLE precalc_copy_filter_bib_list ON COMMIT DROP AS
+        SELECT id, copy FROM precalc_circ_mod_filter_bib_list
+            INTERSECT
+        SELECT id, copy FROM precalc_location_filter_bib_list;
+
+    -- Bring the collapsed filter lists together
+    DROP TABLE IF EXISTS precalc_filter_bib_list;
+    CREATE TEMP TABLE precalc_filter_bib_list ON COMMIT DROP AS
+        SELECT id FROM precalc_bib_filter_bib_list
+            INTERSECT
+        SELECT id FROM precalc_copy_filter_bib_list;
+
+    CREATE INDEX precalc_filter_bib_list_idx
+        ON precalc_filter_bib_list (id);
+
+    IF setup_only THEN
+        RETURN;
+    END IF;
+
+    -- If it's a fixed-rating badge, just do it ...
+    IF badge_row.fixed_rating IS NOT NULL THEN
+        DELETE FROM rating.record_badge_score WHERE badge = badge_id;
+        EXECUTE $e$
+            INSERT INTO rating.record_badge_score (record, badge, score)
+                SELECT record, $1, $2 FROM $e$ || param.func || $e$($1)$e$
+        USING badge_id, badge_row.fixed_rating;
+
+        UPDATE rating.badge SET last_calc = NOW() WHERE id = badge_id;
+
+        RETURN;
+    END IF;
+    -- else, calculate!
+
+    -- Make a session-local scratchpad for calculating scores
+    CREATE TEMP TABLE record_score_scratchpad (
+        bib     BIGINT,
+        value   NUMERIC
+    ) ON COMMIT DROP;
+
+    -- Gather raw values
+    EXECUTE $e$
+        INSERT INTO record_score_scratchpad (bib, value)
+            SELECT * FROM $e$ || param.func || $e$($1)$e$
+    USING badge_id;
+
+    IF badge_row.discard > 0 OR badge_row.percentile IS NOT NULL THEN
+        -- To speed up discard-common
+        CREATE INDEX record_score_scratchpad_score_idx ON record_score_scratchpad (value);
+        ANALYZE record_score_scratchpad;
+    END IF;
+
+    IF badge_row.discard > 0 THEN -- Remove common low values (trim the long tail)
+        DELETE FROM record_score_scratchpad WHERE value IN (
+            SELECT DISTINCT value FROM record_score_scratchpad ORDER BY value LIMIT badge_row.discard
+        );
+    END IF;
+
+    IF badge_row.percentile IS NOT NULL THEN -- Cut population down to exceptional records
+        DELETE FROM record_score_scratchpad WHERE value <= (
+            SELECT value FROM (
+                SELECT  value,
+                        CUME_DIST() OVER (ORDER BY value) AS p
+                  FROM  record_score_scratchpad
+            ) x WHERE p < badge_row.percentile / 100.0 ORDER BY p DESC LIMIT 1
+        );
+    END IF;
+
+
+    -- And, finally, push new data in
+    DELETE FROM rating.record_badge_score WHERE badge = badge_id;
+    INSERT INTO rating.record_badge_score (badge, record, score)
+        SELECT  badge_id,
+                bib,
+                GREATEST(ROUND((CUME_DIST() OVER (ORDER BY value)) * 5), 1) AS value
+          FROM  record_score_scratchpad;
+
+    DROP TABLE record_score_scratchpad;
+
+    -- Now, finally-finally, mark the badge as recalculated
+    UPDATE rating.badge SET last_calc = NOW() WHERE id = badge_id;
+
+    RETURN;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION rating.holds_filled_over_time(badge_id INT)
+    RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
+DECLARE
+    badge   rating.badge_with_orgs%ROWTYPE;
+    iage    INT     := 1;
+    iint    INT     := NULL;
+    iscale  NUMERIC := NULL;
+BEGIN
+
+    SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
+
+    IF badge.horizon_age IS NULL THEN
+        RAISE EXCEPTION 'Badge "%" with id % requires a horizon age but has none.',
+            badge.name,
+            badge.id;
+    END IF;
+
+    PERFORM rating.precalc_bibs_by_copy(badge_id);
+
+    SET LOCAL client_min_messages = error;
+    DROP TABLE IF EXISTS precalc_bib_list;
+    CREATE TEMP TABLE precalc_bib_list ON COMMIT DROP AS
+        SELECT id FROM precalc_filter_bib_list
+            INTERSECT
+        SELECT id FROM precalc_bibs_by_copy_list;
+
+    iint := EXTRACT(EPOCH FROM badge.importance_interval);
+    IF badge.importance_age IS NOT NULL THEN
+        iage := (EXTRACT(EPOCH FROM badge.importance_age) / iint)::INT;
+    END IF;
+
+    -- if iscale is smaller than 1, scaling slope will be shallow ... BEWARE!
+    iscale := COALESCE(badge.importance_scale, 1.0);
+
+    RETURN QUERY
+     SELECT bib,
+            SUM( holds * GREATEST( iscale * (iage - hage), 1.0 ))
+      FROM (
+         SELECT f.id AS bib,
+                (1 + EXTRACT(EPOCH FROM AGE(h.fulfillment_time)) / iint)::INT AS hage,
+                COUNT(h.id)::INT AS holds
+          FROM  action.hold_request h
+                JOIN reporter.hold_request_record rhrr ON (rhrr.id = h.id)
+                JOIN precalc_bib_list f ON (f.id = rhrr.bib_record)
+          WHERE h.fulfillment_time >= NOW() - badge.horizon_age
+                AND h.request_lib = ANY (badge.orgs)
+          GROUP BY 1, 2
+      ) x
+      GROUP BY 1;
+END;
+$f$ LANGUAGE PLPGSQL STRICT;
+
+CREATE OR REPLACE FUNCTION rating.holds_placed_over_time(badge_id INT)
+    RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
+DECLARE
+    badge   rating.badge_with_orgs%ROWTYPE;
+    iage    INT     := 1;
+    iint    INT     := NULL;
+    iscale  NUMERIC := NULL;
+BEGIN
+
+    SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
+
+    IF badge.horizon_age IS NULL THEN
+        RAISE EXCEPTION 'Badge "%" with id % requires a horizon age but has none.',
+            badge.name,
+            badge.id;
+    END IF;
+
+    PERFORM rating.precalc_bibs_by_copy(badge_id);
+
+    SET LOCAL client_min_messages = error;
+    DROP TABLE IF EXISTS precalc_bib_list;
+    CREATE TEMP TABLE precalc_bib_list ON COMMIT DROP AS
+        SELECT id FROM precalc_filter_bib_list
+            INTERSECT
+        SELECT id FROM precalc_bibs_by_copy_list;
+
+    iint := EXTRACT(EPOCH FROM badge.importance_interval);
+    IF badge.importance_age IS NOT NULL THEN
+        iage := (EXTRACT(EPOCH FROM badge.importance_age) / iint)::INT;
+    END IF;
+
+    -- if iscale is smaller than 1, scaling slope will be shallow ... BEWARE!
+    iscale := COALESCE(badge.importance_scale, 1.0);
+
+    RETURN QUERY
+     SELECT bib,
+            SUM( holds * GREATEST( iscale * (iage - hage), 1.0 ))
+      FROM (
+         SELECT f.id AS bib,
+                (1 + EXTRACT(EPOCH FROM AGE(h.request_time)) / iint)::INT AS hage,
+                COUNT(h.id)::INT AS holds
+          FROM  action.hold_request h
+                JOIN reporter.hold_request_record rhrr ON (rhrr.id = h.id)
+                JOIN precalc_bib_list f ON (f.id = rhrr.bib_record)
+          WHERE h.request_time >= NOW() - badge.horizon_age
+                AND h.request_lib = ANY (badge.orgs)
+          GROUP BY 1, 2
+      ) x
+      GROUP BY 1;
+END;
+$f$ LANGUAGE PLPGSQL STRICT;
+
+CREATE OR REPLACE FUNCTION rating.current_hold_count(badge_id INT)
+    RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
+DECLARE
+    badge   rating.badge_with_orgs%ROWTYPE;
+BEGIN
+
+    SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
+
+    PERFORM rating.precalc_bibs_by_copy(badge_id);
+
+    DELETE FROM precalc_copy_filter_bib_list WHERE id NOT IN (
+        SELECT id FROM precalc_filter_bib_list
+            INTERSECT
+        SELECT id FROM precalc_bibs_by_copy_list
+    );
+
+    ANALYZE precalc_copy_filter_bib_list;
+
+    RETURN QUERY
+     SELECT rhrr.bib_record AS bib,
+            COUNT(DISTINCT h.id)::NUMERIC AS holds
+      FROM  action.hold_request h
+            JOIN reporter.hold_request_record rhrr ON (rhrr.id = h.id)
+            JOIN action.hold_copy_map m ON (m.hold = h.id)
+            JOIN precalc_copy_filter_bib_list cf ON (rhrr.bib_record = cf.id AND m.target_copy = cf.copy)
+      WHERE h.fulfillment_time IS NULL
+            AND h.request_lib = ANY (badge.orgs)
+      GROUP BY 1;
+END;
+$f$ LANGUAGE PLPGSQL STRICT;
+
+CREATE OR REPLACE FUNCTION rating.circs_over_time(badge_id INT)
+    RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
+DECLARE
+    badge   rating.badge_with_orgs%ROWTYPE;
+    iage    INT     := 1;
+    iint    INT     := NULL;
+    iscale  NUMERIC := NULL;
+BEGIN
+
+    SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
+
+    IF badge.horizon_age IS NULL THEN
+        RAISE EXCEPTION 'Badge "%" with id % requires a horizon age but has none.',
+            badge.name,
+            badge.id;
+    END IF;
+
+    PERFORM rating.precalc_bibs_by_copy(badge_id);
+
+    DELETE FROM precalc_copy_filter_bib_list WHERE id NOT IN (
+        SELECT id FROM precalc_filter_bib_list
+            INTERSECT
+        SELECT id FROM precalc_bibs_by_copy_list
+    );
+
+    ANALYZE precalc_copy_filter_bib_list;
+
+    iint := EXTRACT(EPOCH FROM badge.importance_interval);
+    IF badge.importance_age IS NOT NULL THEN
+        iage := (EXTRACT(EPOCH FROM badge.importance_age) / iint)::INT;
+    END IF;
+
+    -- if iscale is smaller than 1, scaling slope will be shallow ... BEWARE!
+    iscale := COALESCE(badge.importance_scale, 1.0);
+
+    RETURN QUERY
+     SELECT bib,
+            SUM( circs * GREATEST( iscale * (iage - cage), 1.0 ))
+      FROM (
+         SELECT cn.record AS bib,
+                (1 + EXTRACT(EPOCH FROM AGE(c.xact_start)) / iint)::INT AS cage,
+                COUNT(c.id)::INT AS circs
+          FROM  action.circulation c
+                JOIN precalc_copy_filter_bib_list cf ON (c.target_copy = cf.copy)
+                JOIN asset.copy cp ON (cp.id = c.target_copy)
+                JOIN asset.call_number cn ON (cn.id = cp.call_number)
+          WHERE c.xact_start >= NOW() - badge.horizon_age
+                AND cn.owning_lib = ANY (badge.orgs)
+                AND c.phone_renewal IS FALSE  -- we don't count renewals
+                AND c.desk_renewal IS FALSE
+                AND c.opac_renewal IS FALSE
+          GROUP BY 1, 2
+      ) x
+      GROUP BY 1;
+END;
+$f$ LANGUAGE PLPGSQL STRICT;
+
+CREATE OR REPLACE FUNCTION rating.current_circ_count(badge_id INT)
+    RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
+DECLARE
+    badge   rating.badge_with_orgs%ROWTYPE;
+BEGIN
+
+    SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
+
+    PERFORM rating.precalc_bibs_by_copy(badge_id);
+
+    DELETE FROM precalc_copy_filter_bib_list WHERE id NOT IN (
+        SELECT id FROM precalc_filter_bib_list
+            INTERSECT
+        SELECT id FROM precalc_bibs_by_copy_list
+    );
+
+    ANALYZE precalc_copy_filter_bib_list;
+
+    RETURN QUERY
+     SELECT cn.record AS bib,
+            COUNT(c.id)::NUMERIC AS circs
+      FROM  action.circulation c
+            JOIN precalc_copy_filter_bib_list cf ON (c.target_copy = cf.copy)
+            JOIN asset.copy cp ON (cp.id = c.target_copy)
+            JOIN asset.call_number cn ON (cn.id = cp.call_number)
+      WHERE c.checkin_time IS NULL
+            AND cn.owning_lib = ANY (badge.orgs)
+      GROUP BY 1;
+
+END;
+$f$ LANGUAGE PLPGSQL STRICT;
+
+CREATE OR REPLACE FUNCTION rating.checked_out_total_ratio(badge_id INT)
+    RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
+DECLARE
+    badge   rating.badge_with_orgs%ROWTYPE;
+BEGIN
+
+    SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
+
+    PERFORM rating.precalc_bibs_by_copy(badge_id);
+
+    DELETE FROM precalc_copy_filter_bib_list WHERE id NOT IN (
+        SELECT id FROM precalc_filter_bib_list
+            INTERSECT
+        SELECT id FROM precalc_bibs_by_copy_list
+    );
+
+    ANALYZE precalc_copy_filter_bib_list;
+
+    RETURN QUERY
+     SELECT bib,
+            SUM(checked_out)::NUMERIC / SUM(total)::NUMERIC
+      FROM  (SELECT cn.record AS bib,
+                    (cp.status = 1)::INT AS checked_out,
+                    1 AS total
+              FROM  asset.copy cp
+                    JOIN precalc_copy_filter_bib_list c ON (cp.id = c.copy)
+                    JOIN asset.call_number cn ON (cn.id = cp.call_number)
+              WHERE cn.owning_lib = ANY (badge.orgs)
+            ) x
+      GROUP BY 1;
+END;
+$f$ LANGUAGE PLPGSQL STRICT;
+
+CREATE OR REPLACE FUNCTION rating.holds_total_ratio(badge_id INT)
+    RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
+DECLARE
+    badge   rating.badge_with_orgs%ROWTYPE;
+BEGIN
+
+    SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
+
+    PERFORM rating.precalc_bibs_by_copy(badge_id);
+
+    DELETE FROM precalc_copy_filter_bib_list WHERE id NOT IN (
+        SELECT id FROM precalc_filter_bib_list
+            INTERSECT
+        SELECT id FROM precalc_bibs_by_copy_list
+    );
+
+    ANALYZE precalc_copy_filter_bib_list;
+
+    RETURN QUERY
+     SELECT cn.record AS bib,
+            COUNT(DISTINCT m.hold)::NUMERIC / COUNT(DISTINCT cp.id)::NUMERIC
+      FROM  asset.copy cp
+            JOIN precalc_copy_filter_bib_list c ON (cp.id = c.copy)
+            JOIN asset.call_number cn ON (cn.id = cp.call_number)
+            JOIN action.hold_copy_map m ON (m.target_copy = cp.id)
+      WHERE cn.owning_lib = ANY (badge.orgs)
+      GROUP BY 1;
+END;
+$f$ LANGUAGE PLPGSQL STRICT;
+
+CREATE OR REPLACE FUNCTION rating.holds_holdable_ratio(badge_id INT)
+    RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
+DECLARE
+    badge   rating.badge_with_orgs%ROWTYPE;
+BEGIN
+
+    SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
+
+    PERFORM rating.precalc_bibs_by_copy(badge_id);
+
+    DELETE FROM precalc_copy_filter_bib_list WHERE id NOT IN (
+        SELECT id FROM precalc_filter_bib_list
+            INTERSECT
+        SELECT id FROM precalc_bibs_by_copy_list
+    );
+
+    ANALYZE precalc_copy_filter_bib_list;
+
+    RETURN QUERY
+     SELECT cn.record AS bib,
+            COUNT(DISTINCT m.hold)::NUMERIC / COUNT(DISTINCT cp.id)::NUMERIC
+      FROM  asset.copy cp
+            JOIN precalc_copy_filter_bib_list c ON (cp.id = c.copy)
+            JOIN asset.copy_location cl ON (cl.id = cp.location)
+            JOIN config.copy_status cs ON (cs.id = cp.status)
+            JOIN asset.call_number cn ON (cn.id = cp.call_number)
+            JOIN action.hold_copy_map m ON (m.target_copy = cp.id)
+      WHERE cn.owning_lib = ANY (badge.orgs)
+            AND cp.holdable IS TRUE
+            AND cl.holdable IS TRUE
+            AND cs.holdable IS TRUE
+      GROUP BY 1;
+END;
+$f$ LANGUAGE PLPGSQL STRICT;
+
+CREATE OR REPLACE FUNCTION rating.bib_record_age(badge_id INT)
+    RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
+DECLARE
+    badge   rating.badge_with_orgs%ROWTYPE;
+BEGIN
+
+    SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
+
+    PERFORM rating.precalc_bibs_by_copy_or_uri(badge_id);
+
+    SET LOCAL client_min_messages = error;
+    DROP TABLE IF EXISTS precalc_bib_list;
+    CREATE TEMP TABLE precalc_bib_list ON COMMIT DROP AS
+        SELECT id FROM precalc_filter_bib_list
+            INTERSECT
+        SELECT id FROM precalc_bibs_by_copy_or_uri_list;
+
+    RETURN QUERY
+     SELECT b.id,
+            1.0 / EXTRACT(EPOCH FROM AGE(b.create_date))::NUMERIC + 1.0
+      FROM  precalc_bib_list pop
+            JOIN biblio.record_entry b ON (b.id = pop.id);
+END;
+$f$ LANGUAGE PLPGSQL STRICT;
+
+CREATE OR REPLACE FUNCTION rating.bib_pub_age(badge_id INT)
+    RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
+DECLARE
+    badge   rating.badge_with_orgs%ROWTYPE;
+BEGIN
+
+    SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
+
+    PERFORM rating.precalc_bibs_by_copy_or_uri(badge_id);
+
+    SET LOCAL client_min_messages = error;
+    DROP TABLE IF EXISTS precalc_bib_list;
+    CREATE TEMP TABLE precalc_bib_list ON COMMIT DROP AS
+        SELECT id FROM precalc_filter_bib_list
+            INTERSECT
+        SELECT id FROM precalc_bibs_by_copy_or_uri_list;
+
+    RETURN QUERY
+     SELECT pop.id AS bib,
+            s.value::NUMERIC
+      FROM  precalc_bib_list pop
+            JOIN metabib.record_sorter s ON (
+                s.source = pop.id
+                AND s.attr = 'pubdate'
+                AND s.value ~ '^\d+$'
+            )
+      WHERE s.value::INT <= EXTRACT(YEAR FROM NOW())::INT;
+END;
+$f$ LANGUAGE PLPGSQL STRICT;
+
+CREATE OR REPLACE FUNCTION rating.percent_time_circulating(badge_id INT)
+    RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
+DECLARE
+    badge   rating.badge_with_orgs%ROWTYPE;
+BEGIN
+
+    SELECT * INTO badge FROM rating.badge_with_orgs WHERE id = badge_id;
+
+    PERFORM rating.precalc_bibs_by_copy(badge_id);
+
+    DELETE FROM precalc_copy_filter_bib_list WHERE id NOT IN (
+        SELECT id FROM precalc_filter_bib_list
+            INTERSECT
+        SELECT id FROM precalc_bibs_by_copy_list
+    );
+
+    ANALYZE precalc_copy_filter_bib_list;
+
+    RETURN QUERY
+     SELECT bib,
+            SUM(COALESCE(circ_time,0))::NUMERIC / SUM(age)::NUMERIC
+      FROM  (SELECT cn.record AS bib,
+                    cp.id,
+                    EXTRACT( EPOCH FROM AGE(cp.active_date) ) + 1 AS age,
+                    SUM(  -- time copy spent circulating
+                        EXTRACT(
+                            EPOCH FROM
+                            AGE(
+                                COALESCE(circ.checkin_time, circ.stop_fines_time, NOW()),
+                                circ.xact_start
+                            )
+                        )
+                    )::NUMERIC AS circ_time
+              FROM  asset.copy cp
+                    JOIN precalc_copy_filter_bib_list c ON (cp.id = c.copy)
+                    JOIN asset.call_number cn ON (cn.id = cp.call_number)
+                    LEFT JOIN action.all_circulation circ ON (
+                        circ.target_copy = cp.id
+                        AND stop_fines NOT IN (
+                            'LOST',
+                            'LONGOVERDUE',
+                            'CLAIMSRETURNED',
+                            'LONGOVERDUE'
+                        )
+                        AND NOT (
+                            checkin_time IS NULL AND
+                            stop_fines = 'MAXFINES'
+                        )
+                    )
+              WHERE cn.owning_lib = ANY (badge.orgs)
+                    AND cp.active_date IS NOT NULL
+                    -- Next line requires that copies with no circs (circ.id IS NULL) also not be deleted
+                    AND ((circ.id IS NULL AND NOT cp.deleted) OR circ.id IS NOT NULL)
+              GROUP BY 1,2,3
+            ) x
+      GROUP BY 1;
+END;
+$f$ LANGUAGE PLPGSQL STRICT;
+
+CREATE OR REPLACE FUNCTION rating.generic_fixed_rating_by_copy(badge_id INT)
+    RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
+BEGIN
+    PERFORM rating.precalc_bibs_by_copy(badge_id);
+    RETURN QUERY
+        SELECT id, 1.0 FROM precalc_filter_bib_list
+            INTERSECT
+        SELECT id, 1.0 FROM precalc_bibs_by_copy_list;
+END;
+$f$ LANGUAGE PLPGSQL STRICT;
+
+CREATE OR REPLACE FUNCTION rating.generic_fixed_rating_by_uri(badge_id INT)
+    RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
+BEGIN
+    PERFORM rating.precalc_bibs_by_uri(badge_id);
+    RETURN QUERY
+        SELECT id, 1.0 FROM precalc_bib_filter_bib_list
+            INTERSECT
+        SELECT id, 1.0 FROM precalc_bibs_by_uri_list;
+END;
+$f$ LANGUAGE PLPGSQL STRICT;
+
+CREATE OR REPLACE FUNCTION rating.generic_fixed_rating_by_copy_or_uri(badge_id INT)
+    RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
+BEGIN
+    PERFORM rating.precalc_bibs_by_copy_or_uri(badge_id);
+    RETURN QUERY
+        (SELECT id, 1.0 FROM precalc_filter_bib_list
+            INTERSECT
+        SELECT id, 1.0 FROM precalc_bibs_by_copy_list)
+            UNION
+        (SELECT id, 1.0 FROM precalc_bib_filter_bib_list
+            INTERSECT
+        SELECT id, 1.0 FROM precalc_bibs_by_uri_list);
+END;
+$f$ LANGUAGE PLPGSQL STRICT;
+
+CREATE OR REPLACE FUNCTION rating.generic_fixed_rating_global(badge_id INT)
+    RETURNS TABLE (record BIGINT, value NUMERIC) AS $f$
+BEGIN
+    RETURN QUERY
+        SELECT id, 1.0 FROM precalc_bib_filter_bib_list;
+END;
+$f$ LANGUAGE PLPGSQL STRICT;
+
+CREATE INDEX hold_fulfillment_time_idx ON action.hold_request (fulfillment_time) WHERE fulfillment_time IS NOT NULL;
+CREATE INDEX hold_request_time_idx ON action.hold_request (request_time);
+
+COMMIT;
+

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

Summary of changes:
 Open-ILS/examples/fm_IDL.xml                       |   92 ++
 .../lib/OpenILS/Application/Search/Biblio.pm       |    4 +-
 .../Application/Storage/Driver/Pg/QueryParser.pm   |  124 +++-
 .../Application/Storage/Publisher/biblio.pm        |   27 +
 .../Application/Storage/Publisher/metabib.pm       |   73 ++-
 .../src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm    |    5 +
 .../perlmods/lib/OpenILS/WWW/EGCatLoader/Record.pm |   15 +
 .../perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm |   12 +-
 Open-ILS/src/sql/Pg/002.schema.config.sql          |    2 +-
 Open-ILS/src/sql/Pg/030.schema.metabib.sql         |    6 +-
 Open-ILS/src/sql/Pg/090.schema.action.sql          |    2 +
 Open-ILS/src/sql/Pg/220.schema.rating.sql          |  905 ++++++++++++++++++
 Open-ILS/src/sql/Pg/300.schema.staged_search.sql   |   10 +-
 Open-ILS/src/sql/Pg/950.data.seed-values.sql       |   24 +
 Open-ILS/src/sql/Pg/reporter-schema.sql            |   92 ++-
 Open-ILS/src/sql/Pg/sql_file_manifest              |    2 +
 ...549505_statistical_popularity_infrastructure.pg |   33 +
 .../Pg/upgrade/0983.schema.statistical-ratings.sql | 1005 ++++++++++++++++++++
 .../src/sql/Pg/upgrade/0984.function.qp_search.sql |  606 ++++++++++++
 .../src/support-scripts/badge_score_generator.pl   |   60 ++
 .../src/templates/opac/parts/advanced/search.tt2   |    6 +-
 Open-ILS/src/templates/opac/parts/filtersort.tt2   |   12 +
 .../src/templates/opac/parts/record/summary.tt2    |   10 +
 Open-ILS/src/templates/opac/parts/result/table.tt2 |    9 +-
 Open-ILS/src/templates/opac/parts/searchbar.tt2    |    7 +-
 .../templates/staff/admin/local/rating/badge.tt2   |   60 ++
 .../staff/admin/local/rating/edit_badge.tt2        |   42 +
 .../src/templates/staff/admin/local/t_splash.tt2   |    1 +
 Open-ILS/src/templates/staff/css/style.css.tt2     |    3 +
 .../ui/default/staff/admin/local/rating/badge.js   |  189 ++++
 docs/RELEASE_NOTES_NEXT/OPAC/popularity-rating.txt |   98 ++
 31 files changed, 3493 insertions(+), 43 deletions(-)
 create mode 100644 Open-ILS/src/sql/Pg/220.schema.rating.sql
 create mode 100644 Open-ILS/src/sql/Pg/t/regress/lp1549505_statistical_popularity_infrastructure.pg
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/0983.schema.statistical-ratings.sql
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/0984.function.qp_search.sql
 create mode 100755 Open-ILS/src/support-scripts/badge_score_generator.pl
 create mode 100644 Open-ILS/src/templates/staff/admin/local/rating/badge.tt2
 create mode 100644 Open-ILS/src/templates/staff/admin/local/rating/edit_badge.tt2
 create mode 100644 Open-ILS/web/js/ui/default/staff/admin/local/rating/badge.js
 create mode 100644 docs/RELEASE_NOTES_NEXT/OPAC/popularity-rating.txt


hooks/post-receive
-- 
Evergreen ILS


More information about the open-ils-commits mailing list