LP#1429317 Modify sort of holdings in record view user/ldw/RT16023_to_LP1429317
authorLiam Whalen <liam.whalen@bc.libraries.coop>
Fri, 11 Oct 2013 19:31:55 +0000 (12:31 -0700)
committerLiam Whalen <liam.whalen@bc.libraries.coop>
Fri, 10 Apr 2015 17:54:51 +0000 (10:54 -0700)
Currenlty, holdings in the record and search resutls are ordered via
their call number.  When searching at a higher org unit, the holdings
are first ordered within org units and then ordered according to call
number.

This commit adds the Library Setting opac.holding_sort_type.  This
setting has three posible options.  The first and default if no correct
option is chosen is 'call', which sorts by call number.  The other two
are 'asc' and 'desc'.  'asc' sorts from the oldest item to the newest
item.  'desc' sorts from the newest item to the oldest item.

In order to make this YAOS work with the Staff Client, the commit adds a
value to the TPAC ctx variable.  The value added is ctx.ws_ou.  This
ensures that the correct setting for the Staff Client's workstation is
used rather than the setting for the org unit being searched.

Move MFHD_SUMMARIZED_SUBFIELDS to Const.pm.  The
MFHD_SUMMARIZED_SUBFIELDS constant is defined in
OpenILS/perl/lib/Application/Serial/OPAC.pm.  It is used in the process
of populating the Issues Held drop down for serial's module records.
This commit also uses this variable with its
rank_serial_copy_by_issuance plpgsql function.  In order to minimize
errors where one version of the variable is updated while another is
left as it currently is, I moved the variable to Const.pm.  The code to
dereference the constant hash reference requires two lines.  It would
be nice if it could be done in one, but I could not figure out the
syntax to dereference a constant.

Added rank_ou_root_search_with_pref_lib SQL function.
This SQL function ranks OUs at the same level unless they belong the
the preferred library entered.  It is meant to be used with a search at
the root level of the org tree, so that all org units will be ordered
alphabetically except the pref_lib and its descendants.

This function is needed to sort holdings correctly when searching at the
Consortial level.  As the code is now, when a search is perforemd at the
Consortial level, the org units are sorted according to their distance
from the Consortial OU.  This results in a confusing listing unless the
reader is aware of the org unit tree layout.

In order to fix this, the commit sorts only by org unit name
(alphabetically) when a consortial search is performed.  However, if the user
has a preferred library set, then the sort needs to rank the preferred
OU and its decendants higher than all others, so this new function,
rank_ou_root_search_with_pref_lib does this.  It is a modified version
of rank_ou, with all but the pref_lib logic cut out.

The sorting of items according to the LS opac.holding_sort_type is done
in a number of ways.  First, records are put into two groups.  Pure
serial module records (a record with assets added only via the serials
module) and all other records.  In some cases, a library may put only
serials module items on a record while another library may put regular
items on a record.  This results in a hybrid record.  Hybrid records are
ordered as if they were regular records instead of serials records.

Pure serials module records are ordered via the data in
serial.materialized_holding_code if either 'asc' or 'desc' are chosen.
All other records are ordered by active date then asset.copy id when
'asc' or 'desc' are chosen.  The copy id is used to maintain order when
items have identical active dates.  This can happen with serials module
records. The copy id is needed to keep the holdings of hybrid records
ordered properly.  Because active_date and copy id are used to order
items in 'asc' and 'desc' orders the previous order which ordered items
within call numbers via their copy statuses no longer works.  Items are
ordered strictly by their age within a collection.

If call is chosen for the LS then all records are ordered in the same
manner with their call number determining their order within their org
unit.  In this case, copy status still applies to the order.  In effect,
using the LS of 'call' retains the current functionality of the TPAC.

The ordering of pure serials module's holdings is done with a plpgsql
function named rank_serial_copy_by_issuance.  This function creates a
number that is used by the SQL copy query to maintain the proper
chronological or enumerrative order.

Finally, there is a small bug in the curent TPAC when browsing through
items in the record view.  If the number of items returned is evenly
divisible by the number of copies per page, then the final page of
items will have a Next link that will take the user to a page with no
items and no links to navigate back to the previous items.  This commit
increases the number of items returned by the query for copies by 1, so
the TPAC can determine if there are more items waiting to be displayed,
and if there are then it shows the Next link.  Otherwise, no Next link
is displayed.

The serial.rank_serial_copy_by_issuance function ranks serials added via
the Serials moduel according to the values in
serial.materialized_holding_code.  It currently causes an internal
server error if an issue is encountered that has a combined month e.g
Jan/Feb or Jun/Jul. In these cases the materialized_holding_code is a
value of 01/02 or 06/07 in the above examples.  The function is looking
for an integer value, and the / in the materialized_holding_code is
causing a Postgresql SQL error, which in turn causes the internal server
error in Evergreen.

This fix looks for the first occurance of a string of digits in
materialized_holding_code and uses that as the value to rank the
issuance.  This should preseve the correct order because Jan/Feb will
use 01, while the next issue if it is March will use 03 and still be
placed after Jan/Feb or before it if the order is descending.  The same
thing will occur with enumerative order.

The only unknown at this point is the possibility of a
materialized_holding_code without a numeric value.  This code takes that
into accout, but uses a default value of 0, which will not help order
the issue, but it will stop internal server errors from occuring.

The possiblity of non-numeric materialized_holding_code values needs to
be investigated.

Signed-off-by: Liam Whalen <liam.whalen@bc.libraries.coop>
12 files changed:
Open-ILS/src/perlmods/lib/OpenILS/Application/Serial/OPAC.pm
Open-ILS/src/perlmods/lib/OpenILS/Const.pm
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Record.pm
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Search.pm
Open-ILS/src/sql/Pg/950.data.seed-values.sql
Open-ILS/src/sql/Pg/990.schema.unapi.sql
Open-ILS/src/sql/Pg/upgrade/XXXX.data.org-setting.holding_sort_type.sql [new file with mode: 0644]
Open-ILS/src/sql/Pg/upgrade/XXXX.schema.rank_ou_root_search_with_pref_lib.sql [new file with mode: 0644]
Open-ILS/src/sql/Pg/upgrade/XXXX.schema.rank_serial_copy_by_issuance.sql [new file with mode: 0644]
Open-ILS/src/templates/opac/parts/record/copy_table.tt2
Open-ILS/src/templates/opac/parts/result/table.tt2

index 6422edc..a84d28c 100644 (file)
@@ -16,10 +16,10 @@ use OpenILS::Utils::CStoreEditor q/:funcs/;
 
 my $U = "OpenILS::Application::AppUtils";
 
-my %MFHD_SUMMARIZED_SUBFIELDS = (
-   enum => [ split //, "abcdef" ],   # $g and $h intentionally omitted for now
-   chron => [ split //, "ijklm" ]
-);
+use OpenILS::Const qw/:const/;
+#OILS_MFHD_SUMMARIZED_SUBFIELDS is defined in OpenILS/Const.pm
+my $mfhd_summarized_subfields_ref = OILS_MFHD_SUMMARIZED_SUBFIELDS;
+my %MFHD_SUMMARIZED_SUBFIELDS = %{$mfhd_summarized_subfields_ref};
 
 # This is a helper for scoped_holding_summary_tree_for_bib() a little further down
 
index cd82e83..cb160d3 100644 (file)
@@ -73,6 +73,14 @@ econst OILS_STOP_FINES_MAX_FINES      => 'MAXFINES';
 econst OILS_STOP_FINES_CLAIMS_NEVERCHECKEDOUT => 'CLAIMSNEVERCHECKEDOUT';
 econst OILS_UNLIMITED_CIRC_DURATION   => 'unlimited';
 
+# _____________________________________________________________________
+# Serial's Constants
+# _____________________________________________________________________
+econst OILS_MFHD_SUMMARIZED_SUBFIELDS => {
+            enum => [ split //, "abcdef" ],   # $g and $h intentionally omitted for now
+            chron => [ split //, "ijklm" ]
+        };
+
 # ---------------------------------------------------------------------
 # Settings
 # ---------------------------------------------------------------------
@@ -96,7 +104,7 @@ econst OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN      => 'circ.restore_overdue
 econst OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE          => 'circ.lost_immediately_available';
 econst OILS_SETTING_BLOCK_HOLD_FOR_EXPIRED_PATRON       => 'circ.holds.expired_patron_block';
 econst OILS_SETTING_GENERATE_OVERDUE_ON_LOST_RETURN     => 'circ.lost.generate_overdue_on_checkin';
-
+econst OILS_SETTING_MAX_LIMIT => '5000';
 
 
 
index b1527fb..b943262 100644 (file)
@@ -285,6 +285,8 @@ sub load_common {
             $ctx->{authtoken} = $e->authtoken;
             $ctx->{authtime} = $e->authtime;
             $ctx->{user} = $e->requestor;
+            #In the Staff Client, the org unit registered to the workstation is not always the same as the locg
+            $ctx->{ws_ou} = $ctx->{user}->ws_ou if $ctx->{is_staff};
             $ctx->{place_unfillable} = 1 if $e->requestor->wsid && $e->allowed('PLACE_UNFILLABLE_HOLD', $e->requestor->ws_ou);
 
             # The browser client does not set an OILS-Wrapper header (above).
index cf8ea00..5e7f773 100644 (file)
@@ -5,6 +5,7 @@ use OpenSRF::Utils::Logger qw/$logger/;
 use OpenILS::Utils::CStoreEditor qw/:funcs/;
 use OpenILS::Utils::Fieldmapper;
 use OpenILS::Application::AppUtils;
+use OpenILS::Const qw/:const/;
 use Net::HTTP::NB;
 use IO::Select;
 my $U = 'OpenILS::Application::AppUtils';
@@ -239,10 +240,140 @@ sub mk_copy_query {
     my $copy_offset = shift;
     my $pref_ou = shift;
 
+    #add one to the copy limit, so we can peak ahead in copy_table.tt2 to see if we
+    #should display a Next link
+    $copy_limit = $copy_limit + 1;
+
     my $query = $U->basic_opac_copy_query(
         $rec_id, undef, undef, $copy_limit, $copy_offset, $self->ctx->{is_staff}
     );
 
+    my $lse_org = $org;
+    if ($self->ctx->{is_staff}) {
+        $lse_org = $self->ctx->{ws_ou};
+    }
+
+    #used to determine the paramters for the ORDER BY clauses
+    my $sort_type = $self->ctx->{get_org_setting}->($lse_org, 'opac.holding_sort_type');
+
+    #return the number of non-delted items in the serial.item
+    #table for this record
+    my $sitem_count_query = { 
+        select => {
+            sitem => [{
+                    column => 'id',
+                    transform => 'count',
+                    alias => 'id_count'
+                }]
+        },  
+        from => {
+            sitem => {
+                siss => {
+                    join => {
+                        ssub => {}
+                    }
+                },  
+                sunit => {} 
+            }   
+        },  
+        where => {
+            '+ssub' => {
+                record_entry => $rec_id
+            },
+            '-not' => {
+                '+sunit' => 'deleted'
+            }
+        }
+    };
+
+    #return the number of non-deleted items
+    #in the asset.copy table for the current record
+    my $acp_count_query = {
+        select => {
+            acp => [{
+                    column => 'id',
+                    transform => 'count',
+                    alias => 'id_count'
+                }]
+        },  
+        from => {
+            acp => {
+                acn => {}
+            }   
+        },  
+        where => {
+            '+acn' => {
+                record => $rec_id
+            },  
+            '-not' => {
+                '+acp' => 'deleted'
+            }
+        }
+    };
+
+    my $sitem_count = $self->{editor}->json_query($sitem_count_query);
+    my $acp_count = $self->{editor}->json_query($acp_count_query);
+    my $is_serial = 0;
+
+    #if the number of items in the serial.items table == the number of items in the
+    #asset.copy table for this record then we have a pure serial record containing
+    #only items added through the serials module, so we can use the 
+    #rank_serial_copy_by_issuance function below to sort the items in ascending or
+    #descending order based on the enumeration or chronology data in the subscription.
+    #Otherwise, if sorting by date the code will use active_date.
+    if ($sitem_count->[0]->{'id_count'} == $acp_count->[0]->{'id_count'}) {
+        $is_serial = 1;
+    }
+
+    if ($is_serial) {
+        #this addition to the query adds the necessary fields for the
+        #rank_serial_copy_by_issuance plpgsql function to work properly
+
+        $query->{select}->{aou} = [ {column => 'id', alias => 'ou_id'} ];
+        $query->{select}->{smhc} = [ {column => 'issuance'} ];
+        $query->{from}->{acp}->{sitem} = {
+            fkey => 'id',
+            field => 'unit',
+            join => {
+                smhc => {
+                    fkey => 'issuance',
+                    field => 'issuance',
+                    join => {
+                        siss => {
+                            fkey => 'issuance',
+                            join => {
+                                ssub => {
+                                    fkey => 'subscription',
+                                    join => {
+                                        sdist => {
+                                            fkey => 'id',
+                                            field => 'subscription',
+                                            filter => {
+                                                holding_lib => {
+                                                    in => {
+                                                        select => {aou => [{
+                                                            column => 'id',
+                                                            transform => 'actor.org_unit_descendants',
+                                                            result_field => 'id',
+                                                            params => [$depth]
+                                                        }]},
+                                                        from => 'aou',
+                                                        where => {id => $org}
+                                                    }
+                                                }
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        };
+        $query->{distinct} = 1;
+    }
+
     if($org != $self->ctx->{aou_tree}->()->id) { 
         # no need to add the org join filter if we're not actually filtering
         $query->{from}->{acp}->{aou} = {
@@ -263,20 +394,156 @@ sub mk_copy_query {
                 }
             }
         };
-    };
+    }
 
     # Unsure if we want these in the shared function, leaving here for now
-    unshift(@{$query->{order_by}},
-        { class => "aou", field => 'id',
-          transform => 'evergreen.rank_ou', params => [$org, $pref_ou]
+
+    if ($is_serial) {
+        #create a string to represent the OILS_MFHD_SUMMARIZED_SUBFIELDS constant
+        #defined in OpenILS/Const.pm
+        my $rank_serial_copy_by_issuance_subfields_string = '';
+
+        my $mfhd_summarized_subfields_ref = OILS_MFHD_SUMMARIZED_SUBFIELDS;
+        my %MFHD_SUMMARIZED_SUBFIELDS = %{$mfhd_summarized_subfields_ref};
+
+        #generate a string to be passed in as a parameter to rank_serial_copy_by_issuance
+        #this is to be used by plpgsql to create an HSTORE.  its values are deliminated by a carrot ^.
+        foreach my $key (keys %MFHD_SUMMARIZED_SUBFIELDS) {
+            if ($rank_serial_copy_by_issuance_subfields_string ne '') {
+                $rank_serial_copy_by_issuance_subfields_string .= '^';
+            }
+
+            $rank_serial_copy_by_issuance_subfields_string .= $key;
+            $rank_serial_copy_by_issuance_subfields_string .= '^' . join(',',@{$MFHD_SUMMARIZED_SUBFIELDS{$key}});
         }
-    );
-    push(@{$query->{order_by}},
-        { class => "acp", field => 'status',
-          transform => 'evergreen.rank_cp_status'
+
+        #this code orders pure serials records' items according
+        #to the enumeration or chronology values provided by
+        #the serials module.  rank_serial_copy_by_issuance
+        #calculates the ordering.
+        if ($sort_type eq 'asc' or $sort_type eq 'desc') {
+            unshift(@{$query->{order_by}}, {
+                    class => "smhc", field => "issuance",
+                    transform => 'evergreen.rank_serial_copy_by_issuance',
+                    params => [$rank_serial_copy_by_issuance_subfields_string],
+                    ($sort_type eq 'desc' ? (direction => 'desc') : ())
+                }
+            );
+        
+            #at this point we only have ORDER BY rank_serial_copy_by_issuance
+            #and aou.name and acn.label. We need aou.name to come before
+            #rank_serial_copy_by_issuance, so the org_units are ordered
+            #alphabetically, within their ranking (the code for the rankings
+            #gets unshifted on furhter down.. We remove aou.name and
+            #unshift it back on so it comes before rank_serial_copy_by_issuance.
+            #We do this rather than a simple removal of the second item in the
+            #array because the original array is defined elsewhere and could be
+            #modified independetly of this file.
+            my $index = 0;
+            my @order_by_array = @{$query->{order_by}};
+            my @new_order_by_array = ();
+            foreach my $order_by_clause (@order_by_array) {
+                foreach my $key (keys %{$order_by_clause}) {
+                    if ($key eq 'class' and $order_by_clause->{$key} eq 'aou') {
+                        my $aou_name_order_by = $order_by_clause;
+                        my @end_part_of_query_order_by = @order_by_array[($index + 1)..$#order_by_array];
+
+                        push(@new_order_by_array, @end_part_of_query_order_by);
+                        unshift(@new_order_by_array, $aou_name_order_by);
+
+                        #return memory used by current order_by to Perl
+                        @{$query->{order_by}} = undef;
+                        @order_by_array = undef;
+
+                        $query->{order_by} = \@new_order_by_array;
+                        last;
+                    }
+                }
+
+                if (@order_by_array != undef) {
+                    push(@new_order_by_array, $order_by_clause);
+                }
+
+                $index++;
+            }
+        }
+    }
+
+    my $search_lib_parent = $self->ctx->{get_aou}->($org)->parent_ou;
+
+    #If the search_lib_parent is defined then we are not at the top org_unit.
+    #Otherwise, if we are at the top org unit but the pref_ou is not the
+    #top org unit or we are not searching the top org_unit, then we will rank
+    #the OUs by distance.
+    #However, in the case that we are searching the top OU with a pref_lib
+    #we will only order the org_units within the pref_lib by calling
+    #rank_ou_root_search_with_pref_lib instead of rank_ou.
+    #Otherwise, if we are searching the top OU and the pref_lib is the top OU
+    #then we rank on name only, which is done by skiping this code leaving
+    #aou.name in the ORDER BY without a corresponding OU rank.
+    if (defined $search_lib_parent || $pref_ou != $org) {
+        unshift(@{$query->{order_by}}, ({ 
+                class => "aou", field => 'id',
+                (!defined $search_lib_parent ? 
+                    (transform => 'evergreen.rank_ou_root_search_with_pref_lib', params => [$org, $pref_ou])
+                    :
+                    (transform => 'evergreen.rank_ou', params => [$org, $pref_ou])
+                )
+            })
+        );
+    }
+    
+    push(@{$query->{order_by}}, {
+            class => "acp", field => 'status',
+            transform => 'evergreen.rank_cp_status'
         }
     );
 
+    #IF the LSE sort order is asc or desc then
+    #add the apporpriate active_date ORDER BY.
+    #We add active_date even when sorting pure
+    #serial records via rank_serial_copy_by_issuance,
+    #so that records with multiple copies of the same item
+    #are ordered according to their date.
+    if ($sort_type eq 'asc' or $sort_type eq 'desc') {
+        my $index = 0;
+        foreach my $order_by_clause (@{$query->{order_by}}) {
+            foreach my $key (keys %{$order_by_clause}) {
+                if ($key eq 'class' and $order_by_clause->{$key} eq 'acn') {
+                    my $new_order_by = {
+                        class => 'acp', field => 'active_date',
+                        ($sort_type eq 'desc' ? (direction => 'desc') : ())
+                    };
+                    #this replaces the current order by with a new reference
+                    $order_by_clause = undef;
+                    $order_by_clause = $new_order_by;
+
+                    #add a order by acp.id after active date, so that items
+                    #with identical active_dates (e.g. items added via the serials module)
+                    #are ordered in the way they were added to the database
+                    my $acp_id_order_by = {
+                        class => 'acp', field => 'id',
+                        ($sort_type eq 'desc' ? (direction => 'desc') : ())
+                    };
+
+                    my @order_by_array = @{$query->{order_by}};
+                    my @begining_part_of_query_order_by = @order_by_array[0..$index];
+                    my @end_part_of_query_order_by = @order_by_array[($index + 1)..$#order_by_array];
+
+                    @order_by_array = @begining_part_of_query_order_by;
+                    push(@order_by_array, $acp_id_order_by);
+                    push(@order_by_array, @end_part_of_query_order_by);
+
+                    #return memory used by current order_by to Perl
+                    @{$query->{order_by}} = undef;
+
+                    $query->{order_by} = \@order_by_array;
+                }
+            }
+            $index++;
+        }
+    }
+
     return $query;
 }
 
index 19f3a25..a7cc673 100644 (file)
@@ -486,6 +486,22 @@ sub load_rresults {
     );
     $self->timelog("Returned from get_records_and_facets()");
 
+    #try and get holdings
+    #should these values be passed in to this subroutine?
+    my $copy_offset = 0;
+    my $copy_limit = 5;
+    my $copy_depth = $depth;
+    my $cstore = OpenSRF::AppSession->create('open-ils.cstore');
+    $cstore->session_locale($OpenILS::Utils::CStoreEditor::default_locale);
+    for my $rec_id (@$rec_ids) {
+        my $copy_rec = $cstore->request(
+            'open-ils.cstore.json_query.atomic', 
+            $self->mk_copy_query($rec_id, $ctx->{search_ou}, $copy_depth, $copy_limit, $copy_offset, $ctx->{pref_ou})
+        );
+        my $copies = $copy_rec->gather(1);
+        $ctx->{holdings}->{$rec_id} = $copies;
+    }
+
     if ($page == 0 and @$rec_ids == 1) {
         my $stat = 0;
         if ($is_meta) {
index bac8cfc..05c4591 100644 (file)
@@ -14263,3 +14263,24 @@ INSERT INTO config.org_unit_setting_type
              'coust', 'description'),
          'bool');
 
+INSERT INTO config.org_unit_setting_type
+    (name, label, datatype, description, grp, update_perm, view_perm) 
+VALUES (
+    'opac.holding_sort_type',
+    oils_i18n_gettext(
+        'opac.holding_sort_type',
+        'Specify how items are ordered',
+        'coust',
+        'label'
+    ),
+    'string',
+    oils_i18n_gettext(
+        'opac.holding_sort_type',
+        'This value specifies how items are ordered in search results and record views within the org unit. To sort from newest to oldest by active date use ''desc''. To sort from oldest to newest by active date use ''asc''. To sort by call number use ''call''.',
+        'coust',
+        'description'
+    ),
+    'opac',
+    93,
+    192);
+
index 8ad2ec4..2b2abdb 100644 (file)
@@ -41,6 +41,177 @@ RETURNS INTEGER AS $$
     );
 $$ LANGUAGE SQL STABLE;
 
+CREATE OR REPLACE FUNCTION evergreen.rank_ou_root_search_with_pref_lib(lib INT, search_lib INT, pref_lib INT DEFAULT NULL)
+RETURNS INTEGER AS $$
+
+    -- In the case of ranking OUs in holdings displays
+    -- If the top level OU is the search lib,
+    -- meaning the search_lib's parent_ou is NULL, then
+    -- we only want to give special rankings to items
+    -- related to the pref_lib.
+    SELECT COALESCE(
+
+        -- lib matches pref_lib
+        (SELECT CASE WHEN $1 = $3 THEN -10000 END),
+
+
+        -- pref_lib is a child of search_lib and lib is a child of pref lib.  
+        (SELECT distance - 5000
+            FROM actor.org_unit_descendants_distance($3) 
+            WHERE id = $1 AND $3 IN (
+                SELECT id FROM actor.org_unit_descendants($2))),
+
+        -- all others pay cash
+        1000
+    );
+$$ LANGUAGE SQL STABLE;
+
+CREATE OR REPLACE FUNCTION rank_serial_copy_by_issuance(issuance_id INT, groupings TEXT) RETURNS NUMERIC AS $$
+DECLARE
+    row RECORD;
+    rank NUMERIC;
+    display_subfields TEXT;
+    grouping_array TEXT[];
+    grouping_store HSTORE;
+    more_weight INT;
+    weight INT;
+    weight_length INT;
+    row_text TEXT;
+    row_value INT;
+    rank_decimal_length INT;
+    display_grouping TEXT;
+BEGIN
+
+    -- get the display_grouping so we know how to order this
+    SELECT sdist.display_grouping FROM serial.distribution AS sdist
+        INNER JOIN serial.subscription AS ssub ON (sdist.subscription = ssub.id)
+        INNER JOIN serial.issuance AS siss ON (ssub.id = siss.subscription)
+        WHERE siss.id = issuance_id INTO display_grouping;
+
+    grouping_array := string_to_array(groupings, '^');
+
+    grouping_store := hstore(grouping_array);
+
+    display_subfields := grouping_store->display_grouping;
+
+    -- It is possible for the first level of enumeration to be 0.
+    -- So, we initialize rank to -1 to indicate that it has not been set yet
+    rank := -1;
+
+    -- If we are grouping by chron, but there is no enum defined in this subscription's
+    -- capture patter, then the chron values will be recorded in enum's a-f
+    IF (display_grouping = 'chron') THEN
+        IF (SELECT NOT EXISTS(SELECT value FROM serial.materialized_holding_code WHERE issuance = issuance_id AND subfield IN (SELECT * FROM unnest(string_to_array(display_subfields, ','))))) THEN
+            display_grouping = 'enum';
+            display_subfields := grouping_store->display_grouping;
+        END IF;
+    END IF;
+
+    FOR row IN SELECT regexp_replace(value, '^\D*(\d*).*$', '\1') AS value FROM serial.materialized_holding_code WHERE issuance = issuance_id AND subfield IN (SELECT * FROM unnest(string_to_array(display_subfields, ','))) ORDER BY subfield LOOP
+        row_text := row.value;
+
+        -- strip leading 0s from the subfield values, unless we have a value of 0 or the empty string
+        -- If we have an emptry string, then there was no digit present int materialzed_holding_code, so we default to a value of 0
+        IF (row_text != '0' AND row_text != '') THEN
+            row_value := trim(leading '0' from row_text)::INT;
+        ELSE
+            row_value = 0;
+        END IF;
+
+        IF (rank = -1) THEN
+            rank := row_value;
+        ELSE
+            -- Determine weight.
+            -- Here we multiply by 2 to get an even number, so that when
+            -- we add 1 we are assured of getting an odd number.
+            -- This avoids having a number that ends in 0, which would cause the
+            -- loss of a significant digit because these values are being
+            -- appended after the decimal place.  It also allows us room
+            -- to play with the weight value to ensure it never begins with a 9.
+            weight := row_value * 2 + 1;
+
+            -- Because a row_value of 1 becomes a weight of 3, we can assign a row_value of
+            -- 0 to a weight of 1.  This means 0 values in the MFHD data will be sorted properly.
+            -- If they are left at 0, this information will be lost because the weight is added
+            -- to the manitissa.
+            IF (weight = 0) THEN
+                weight = 1;
+            END IF;
+
+            -- We use 9's to deliminate that start of a new level of
+            -- enumeration or chronology in the mantissa.
+            -- We add a number of 9's equal to that of the legnth of weight (row_value * 2 + 1),
+            -- which is the number we will be appending to the mantissa, to the beginning of weight.
+            weight_length := length(weight::TEXT);
+
+            -- Determine how many 9s to add to the weight.
+            -- If we are adding a level with a value over 9 then we have to add
+            -- a requisite number of 9's in front of the derived weight to ensure the
+            -- rank is valid.  If the value is less than 9, then we do not add
+            -- a 9 because we subtract 10 ^ (legnth of weight - 1) from weights
+            -- derived form values over 9 (This is explained below).
+            -- This means we do not want 9's in front of values < 9 because that would
+            -- move them above items they should follow in the rankings
+            more_weight := 0;
+
+            IF (row_value > 9) THEN
+                -- We loop from 0 to row_length -2 because we are trying to determine how many
+                -- 9s to append to the mantissa.
+                -- For example if row_value is 10, then we want to append one 9 to the mantissa,
+                -- so we go through this loop once.
+                -- We want to append a 9 to the mantisaa to ensure this number is placed before
+                -- numbers underneath 10.
+                -- For instance, if the volume number is 2 and we are dealinig with issue 10,
+                -- then rank is currently = 2.0.  We are going to append a value of
+                -- 10 * 2 + 1 - 10 to rank.
+                -- However, to ensure that 10 comes after numbers < 10 we append a single .9 to the mantissa.
+                -- After this loop is done, in the case of a value of 10, but before we add the final
+                -- calcuated value to rank, we would have a rank of 2.9.  This ensures that this number is always
+                -- going to be greater than 2.03 - 2.19 which would represent volume 2 issues 1 to 9 respectively.
+                -- In the case of 10, the final rank would be 2.911.
+                FOR exponent IN 0 .. (weight_length - 2) LOOP
+                      more_weight := more_weight + (9 * (10 ^ exponent));
+                END LOOP;
+            END IF;
+
+            -- Get the length of the current mantissa so we know how many 0's to place in front
+            -- of more_weight.
+            rank_decimal_length := length(rtrim(split_part(rank::TEXT, '.', 2), '0'));
+
+            -- Subtracting 10 ^ (legnth of weight - 1) from weight ensures that weight never starts with a 9.
+            -- We never add a weight starting with a 9 because 9 represents a new level in the enumeration
+            -- or chronology.  When row_value is less than 10 we have a special case and weight is not decremented.
+            -- In these cases 1 = 3, 2 = 5 .. 9 = 19, values less than 9 do not have 9's prepended to them.
+            IF (row_value > 9) THEN
+                weight := weight - (10 ^ (length(weight::TEXT) - 1));
+            END IF;
+
+            -- Add a number of 0's to the end of more_weight so that weight
+            -- is appended to more_weight
+            IF (more_weight > 0) THEN
+                more_weight := more_weight * (10 ^ (length(weight::TEXT)));
+
+                weight := weight + more_weight;
+            END IF;
+
+            -- The rank_decimal_length + length of weight tells us how many decimal places to shift
+            -- weight, so it will be added to the end of the mantissa.
+            IF (weight > 10) THEN
+                rank := rank + (weight * (0.1 ^ (rank_decimal_length + length(weight::TEXT))));
+            ELSE
+                -- If weight is less than 10 then we add one more decimal place
+                -- becasue this means it reperesnts a level from 0 - 4, which we need
+                -- to preceed with a 0, so our weight will be 03, 05, 07, 09, which
+                -- will place them before 5 - 9 which are 11, 13, 15, 17, and 19
+                -- which fall before anyting > 9 because those all start with at least one 9.
+                rank := rank + (weight * (0.1 ^ (rank_decimal_length + length(weight::TEXT) + 1)));
+            END IF;
+        END IF;
+    END LOOP;
+
+  RETURN rank;
+END; $$ LANGUAGE 'plpgsql';
+
 CREATE OR REPLACE FUNCTION evergreen.rank_cp_status(status INT)
 RETURNS INTEGER AS $$
     WITH totally_available AS (
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.org-setting.holding_sort_type.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.org-setting.holding_sort_type.sql
new file mode 100644 (file)
index 0000000..31700bd
--- /dev/null
@@ -0,0 +1,27 @@
+BEGIN;
+
+SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+
+INSERT INTO config.org_unit_setting_type
+    (name, label, datatype, description, grp, update_perm, view_perm) 
+VALUES (
+    'opac.holding_sort_type',
+    oils_i18n_gettext(
+        'opac.holding_sort_type',
+        'Specify how items are ordered',
+        'coust',
+        'label'
+    ),
+    'string',
+    oils_i18n_gettext(
+        'opac.holding_sort_type',
+        'This value specifies how items are ordered in search results and record views within the org unit. To sort from newest to oldest by active date use ''desc''. To sort from oldest to newest by active date use ''asc''. To sort by call number use ''call''.',
+        'coust',
+        'description'
+    ),
+    'opac',
+    93,
+    192
+);
+
+COMMIT;
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.rank_ou_root_search_with_pref_lib.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.rank_ou_root_search_with_pref_lib.sql
new file mode 100644 (file)
index 0000000..c26c54b
--- /dev/null
@@ -0,0 +1,29 @@
+BEGIN;
+
+CREATE OR REPLACE FUNCTION evergreen.rank_ou_root_search_with_pref_lib(lib INT, search_lib INT, pref_lib INT DEFAULT NULL)
+RETURNS INTEGER AS $$
+
+    -- In the case of ranking OUs in holdings displays
+    -- If the top level OU is the search lib,
+    -- meaning the search_lib's parent_ou is NULL, then
+    -- we only want to give special rankings to items
+    -- related to the pref_lib.
+    SELECT COALESCE(
+
+        -- lib matches pref_lib
+        (SELECT CASE WHEN $1 = $3 THEN -10000 END),
+
+
+        -- pref_lib is a child of search_lib and lib is a child of pref lib.  
+        (SELECT distance - 5000
+            FROM actor.org_unit_descendants_distance($3) 
+            WHERE id = $1 AND $3 IN (
+                SELECT id FROM actor.org_unit_descendants($2))),
+
+        -- all others pay cash
+        1000
+    );
+
+$$ LANGUAGE SQL STABLE;
+
+COMMIT;
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.rank_serial_copy_by_issuance.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.rank_serial_copy_by_issuance.sql
new file mode 100644 (file)
index 0000000..01e1945
--- /dev/null
@@ -0,0 +1,149 @@
+BEGIN;
+
+CREATE OR REPLACE FUNCTION rank_serial_copy_by_issuance(issuance_id INT, groupings TEXT) RETURNS NUMERIC AS $$
+DECLARE
+    row RECORD;
+    rank NUMERIC;
+    display_subfields TEXT;
+    grouping_array TEXT[];
+    grouping_store HSTORE;
+    more_weight INT;
+    weight INT;
+    weight_length INT;
+    row_text TEXT;
+    row_value INT;
+    rank_decimal_length INT;
+    display_grouping TEXT;
+BEGIN
+
+    -- get the display_grouping so we know how to order this
+    SELECT sdist.display_grouping FROM serial.distribution AS sdist
+        INNER JOIN serial.subscription AS ssub ON (sdist.subscription = ssub.id)
+        INNER JOIN serial.issuance AS siss ON (ssub.id = siss.subscription)
+        WHERE siss.id = issuance_id INTO display_grouping;
+
+    grouping_array := string_to_array(groupings, '^');
+
+    grouping_store := hstore(grouping_array);
+
+    display_subfields := grouping_store->display_grouping;
+
+    -- It is possible for the first level of enumeration to be 0.
+    -- So, we initialize rank to -1 to indicate that it has not been set yet
+    rank := -1;
+
+    -- If we are grouping by chron, but there is no enum defined in this subscription's
+    -- capture patter, then the chron values will be recorded in enum's a-f
+    IF (display_grouping = 'chron') THEN
+        IF (SELECT NOT EXISTS(SELECT value FROM serial.materialized_holding_code WHERE issuance = issuance_id AND subfield IN (SELECT * FROM unnest(string_to_array(display_subfields, ','))))) THEN
+            display_grouping = 'enum';
+            display_subfields := grouping_store->display_grouping;
+        END IF;
+    END IF;
+
+    FOR row IN SELECT regexp_replace(value, '^\D*(\d*).*$', '\1') AS value FROM serial.materialized_holding_code WHERE issuance = issuance_id AND subfield IN (SELECT * FROM unnest(string_to_array(display_subfields, ','))) ORDER BY subfield LOOP
+        row_text := row.value;
+
+        -- strip leading 0s from the subfield values, unless we have a value of 0 or the empty string
+        -- If we have an emptry string, then there was no digit present int materialzed_holding_code, so we default to a value of 0
+        IF (row_text != '0' AND row_text != '') THEN
+            row_value := trim(leading '0' from row_text)::INT;
+        ELSE
+            row_value = 0;
+        END IF;
+
+        IF (rank = -1) THEN
+            rank := row_value;
+        ELSE
+            -- Determine weight.
+            -- Here we multiply by 2 to get an even number, so that when
+            -- we add 1 we are assured of getting an odd number.
+            -- This avoids having a number that ends in 0, which would cause the
+            -- loss of a significant digit because these values are being
+            -- appended after the decimal place.  It also allows us room
+            -- to play with the weight value to ensure it never begins with a 9.
+            weight := row_value * 2 + 1;
+
+            -- Because a row_value of 1 becomes a weight of 3, we can assign a row_value of
+            -- 0 to a weight of 1.  This means 0 values in the MFHD data will be sorted properly.
+            -- If they are left at 0, this information will be lost because the weight is added
+            -- to the manitissa.
+            IF (weight = 0) THEN
+                weight = 1;
+            END IF;
+
+            -- We use 9's to deliminate that start of a new level of
+            -- enumeration or chronology in the mantissa.
+            -- We add a number of 9's equal to that of the legnth of weight (row_value * 2 + 1),
+            -- which is the number we will be appending to the mantissa, to the beginning of weight.
+            weight_length := length(weight::TEXT);
+
+            -- Determine how many 9s to add to the weight.
+            -- If we are adding a level with a value over 9 then we have to add
+            -- a requisite number of 9's in front of the derived weight to ensure the
+            -- rank is valid.  If the value is less than 9, then we do not add
+            -- a 9 because we subtract 10 ^ (legnth of weight - 1) from weights
+            -- derived form values over 9 (This is explained below).
+            -- This means we do not want 9's in front of values < 9 because that would
+            -- move them above items they should follow in the rankings
+            more_weight := 0;
+
+            IF (row_value > 9) THEN
+                -- We loop from 0 to row_length -2 because we are trying to determine how many
+                -- 9s to append to the mantissa.
+                -- For example if row_value is 10, then we want to append one 9 to the mantissa,
+                -- so we go through this loop once.
+                -- We want to append a 9 to the mantisaa to ensure this number is placed before
+                -- numbers underneath 10.
+                -- For instance, if the volume number is 2 and we are dealinig with issue 10,
+                -- then rank is currently = 2.0.  We are going to append a value of
+                -- 10 * 2 + 1 - 10 to rank.
+                -- However, to ensure that 10 comes after numbers < 10 we append a single .9 to the mantissa.
+                -- After this loop is done, in the case of a value of 10, but before we add the final
+                -- calcuated value to rank, we would have a rank of 2.9.  This ensures that this number is always
+                -- going to be greater than 2.03 - 2.19 which would represent volume 2 issues 1 to 9 respectively.
+                -- In the case of 10, the final rank would be 2.911.
+                FOR exponent IN 0 .. (weight_length - 2) LOOP
+                      more_weight := more_weight + (9 * (10 ^ exponent));
+                END LOOP;
+            END IF;
+
+            -- Get the length of the current mantissa so we know how many 0's to place in front
+            -- of more_weight.
+            rank_decimal_length := length(rtrim(split_part(rank::TEXT, '.', 2), '0'));
+
+            -- Subtracting 10 ^ (legnth of weight - 1) from weight ensures that weight never starts with a 9.
+            -- We never add a weight starting with a 9 because 9 represents a new level in the enumeration
+            -- or chronology.  When row_value is less than 10 we have a special case and weight is not decremented.
+            -- In these cases 1 = 3, 2 = 5 .. 9 = 19, values less than 9 do not have 9's prepended to them.
+            IF (row_value > 9) THEN
+                weight := weight - (10 ^ (length(weight::TEXT) - 1));
+            END IF;
+
+            -- Add a number of 0's to the end of more_weight so that weight
+            -- is appended to more_weight
+            IF (more_weight > 0) THEN
+                more_weight := more_weight * (10 ^ (length(weight::TEXT)));
+
+                weight := weight + more_weight;
+            END IF;
+
+            -- The rank_decimal_length + length of weight tells us how many decimal places to shift
+            -- weight, so it will be added to the end of the mantissa.
+            IF (weight > 10) THEN
+                rank := rank + (weight * (0.1 ^ (rank_decimal_length + length(weight::TEXT))));
+            ELSE
+                -- If weight is less than 10 then we add one more decimal place
+                -- becasue this means it reperesnts a level from 0 - 4, which we need
+                -- to preceed with a 0, so our weight will be 03, 05, 07, 09, which
+                -- will place them before 5 - 9 which are 11, 13, 15, 17, and 19
+                -- which fall before anyting > 9 because those all start with at least one 9.
+                rank := rank + (weight * (0.1 ^ (rank_decimal_length + length(weight::TEXT) + 1)));
+            END IF;
+        END IF;
+    END LOOP;
+
+  RETURN rank;
+END; $$ LANGUAGE 'plpgsql';
+
+COMMIT;
index eeb8643..66c9507 100644 (file)
@@ -75,7 +75,16 @@ END;
 END; # FOREACH bib
 -%]
         [%- last_cn = 0;
+        count = 0;
+        copies_max = copies.max;
+        copies_size = copies.size;
         FOR copy_info IN copies;
+            IF copies_size > ctx.copy_limit;
+                IF copies_max == count;
+                    LAST;
+                END;
+                count = count + 1;
+            END;
             callnum = copy_info.call_number_label;
             NEXT IF callnum == '##URI##';
 
@@ -259,7 +268,7 @@ END; # FOREACH bib
                     l('Previous [_1]', ctx.copy_offset - new_offset) %]</a>
             </td>
         [%- END %]
-        [%- IF copies.size >= ctx.copy_limit AND NOT serial_holdings %]
+        [%- IF copies.size > ctx.copy_limit AND NOT serial_holdings %]
             <td>
                 <a href="[% mkurl('', {copy_offset => ctx.copy_offset + ctx.copy_limit, copy_limit => ctx.copy_limit}) %]">[%
                     l('Next [_1]', ctx.copy_limit) %] &raquo;</a>
index 6cbe41e..d61310c 100644 (file)
@@ -301,15 +301,16 @@ END;
                                                                 </td>
                                                                 <td><a href="[% uri.href %]">[% uri.link | html %]</a>[% ' - ' _ uri.note | html IF uri.note %]</td>
                                                             </tr>
-                                                            [% END %]
-                                                            [%- IF args.holdings.size > 0;
-                                                                FOREACH copy IN args.holdings;
-                                                                    IF copy.part_label != '';
-                                                                        has_parts = 'true';
-                                                                        LAST;
-                                                                    END;
+                                                        [% END %]
+                                                        [%-
+                                                        rec_id = rec.id;
+                                                        IF ctx.holdings.$rec_id.size > 0;
+                                                            FOREACH copy IN ctx.holdings.$rec_id;
+                                                                IF copy.part_label != '';
+                                                                    has_parts = 'true';
+                                                                    LAST;
                                                                 END;
-                                                            %]
+                                                            END %]
                                                             <tr name='bib_cn_list' class='result_table_title_cell'>
                                                                 <td colspan='2'>
                                                                     <table title="[% l('Record Holdings Details') %]"
@@ -324,27 +325,37 @@ END;
                                                                             <th>[% l('Status') %]</th>
                                                                         </tr></thead>
                                                                         <tbody>
-                                                                [% FOR copy IN args.holdings %]
-                                                                        <tr>
-                                                                            <td>
-[%- copy_info = copy;
-    INCLUDE "opac/parts/library_name_link.tt2"; %]
-                                                                            </td>
-                                                                            <td>[% copy.location | html %]</td>
-                                                                            <td>[% copy.label | html %]</td>
-                                                                            [%- IF has_parts == 'true'; %]
-                                                                            <td>[% copy.part_label %]</td>
-                                                                            [%- END %]
-                                                                            <td>[% copy.status | html %]</td>
-                                                                        </tr>
-                                                                [% END %]
+                                                            [%
+                                                            copies = ctx.holdings.$rec_id;
+
+                                                            FOR copy IN copies;
+                                                                callnum = copy.call_number_label;
+                                                                NEXT IF callnum == '##URI##';
+                                                                
+                                                                IF copy.part_label != '';
+                                                                    has_parts = 'true';
+                                                                END %]
+                                                                            <tr>
+                                                                                <td>[% 
+                                                                                        org_name = ctx.get_aou(copy.circ_lib).name;
+                                                                                        org_name | html
+                                                                                %]
+                                                                                </td>
+                                                                                <td>[% copy.copy_location | html %]</td>
+                                                                                <td>[% copy.call_number_label | html %]</td>
+                                                                                [%- IF has_parts == 'true'; %]
+                                                                                    <td>[% copy.part_label %]</td>
+                                                                                [%- END %]
+                                                                                <td>[% copy.copy_status | html %]</td>
+                                                                            </tr>
+                                                            [% END %]
                                                                         </tbody>
                                                                     </table>
                                                                 </td>
                                                             </tr>
                                                             [%- has_parts = 'false';
-                                                                END;
-                                                             %]
+                                                        END;
+                                                        %]
                                                         [% END %] <!-- END detail_record_view -->
                                                     </table>
                                                     [% PROCESS "opac/parts/result/copy_counts.tt2" %]