Merge branch 'master' of git://git.evergreen-ils.org/Evergreen into ttopac
authorevergreen <evergreen@squeeze.debian>
Tue, 28 Jun 2011 15:08:51 +0000 (11:08 -0400)
committerevergreen <evergreen@squeeze.debian>
Tue, 28 Jun 2011 15:08:51 +0000 (11:08 -0400)
88 files changed:
Open-ILS/examples/fm_IDL.xml
Open-ILS/examples/oils_sip.xml.example
Open-ILS/examples/opensrf.xml.example
Open-ILS/src/Makefile.am
Open-ILS/src/apachemods/Makefile.am
Open-ILS/src/extras/Makefile.install
Open-ILS/src/perlmods/Makefile.am
Open-ILS/src/perlmods/lib/OpenILS/Application/Actor/Container.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Cat.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Cat/AssetCommon.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Cat/BibCommon.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Circ.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Search/Serial.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/asset.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/config.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/serial.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/action.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Trigger/Validator.pm
Open-ILS/src/perlmods/lib/OpenILS/Const.pm
Open-ILS/src/perlmods/lib/OpenILS/SIP/Transaction/Checkout.pm
Open-ILS/src/perlmods/lib/OpenILS/Utils/MFHD.pm
Open-ILS/src/perlmods/lib/OpenILS/Utils/PermitHold.pm
Open-ILS/src/perlmods/lib/OpenILS/Utils/TagURI.pm
Open-ILS/src/perlmods/lib/OpenILS/WWW/AddedContent/OpenLibrary.pm
Open-ILS/src/perlmods/lib/OpenILS/WWW/TemplateBatchBibUpdate.pm
Open-ILS/src/perlmods/t/14-OpenILS-Utils.t
Open-ILS/src/sql/Pg/002.schema.config.sql
Open-ILS/src/sql/Pg/040.schema.asset.sql
Open-ILS/src/sql/Pg/099.matrix_weights.sql
Open-ILS/src/sql/Pg/100.circ_matrix.sql
Open-ILS/src/sql/Pg/110.hold_matrix.sql
Open-ILS/src/sql/Pg/210.schema.serials.sql
Open-ILS/src/sql/Pg/950.data.seed-values.sql
Open-ILS/src/sql/Pg/990.schema.unapi.sql
Open-ILS/src/sql/Pg/999.functions.global.sql
Open-ILS/src/sql/Pg/upgrade/0544.data.patron_no_collections.sql
Open-ILS/src/sql/Pg/upgrade/0559.schema.biblio.extract_located_uris.sql
Open-ILS/src/sql/Pg/upgrade/0560.fix_opac_copy_vis_cache.sql [new file with mode: 0644]
Open-ILS/src/sql/Pg/upgrade/0561.schema.tnf_index.sql [new file with mode: 0644]
Open-ILS/src/sql/Pg/upgrade/0562.schema.copy_active_date.sql [new file with mode: 0644]
Open-ILS/src/sql/Pg/upgrade/0563.data.collections_exempt_perm.sql [new file with mode: 0644]
Open-ILS/src/sql/Pg/upgrade/0564.data.org-setting-cat.volume.delete_on_empty.sql [new file with mode: 0644]
Open-ILS/src/sql/Pg/upgrade/0565.schema.action-trigger.event_definition.hold-cancel-no-target-notification.sql [new file with mode: 0644]
Open-ILS/src/sql/Pg/upgrade/0566.schema.unAPI_XMLAGG_cleanup.sql [new file with mode: 0644]
Open-ILS/src/sql/Pg/upgrade/0567.data.ou_setting_generate_overdue_on_lost.sql [new file with mode: 0644]
Open-ILS/src/sql/Pg/upgrade/0568.schema.cache_visibility_speed_boost.sql [new file with mode: 0644]
Open-ILS/web/Makefile.am
Open-ILS/web/conify/global/config/copy_status.html
Open-ILS/web/js/dojo/openils/conify/nls/conify.js
Open-ILS/web/js/ui/default/actor/user/register.js
Open-ILS/web/opac/common/js/config.js
Open-ILS/web/opac/images/openlibrary.gif [new file with mode: 0644]
Open-ILS/web/opac/locale/en-US/lang.dtd
Open-ILS/web/opac/locale/en-US/opac.dtd
Open-ILS/web/opac/skin/craftsman/xml/rdetail/rdetail_cn_details.xml
Open-ILS/web/opac/skin/default/js/copy_details.js
Open-ILS/web/opac/skin/default/js/rdetail.js
Open-ILS/web/opac/skin/default/js/result_common.js
Open-ILS/web/opac/skin/default/xml/rdetail/rdetail_cn_details.xml
Open-ILS/web/opac/skin/default/xml/result/result_table.xml
Open-ILS/web/templates/default/conify/global/config/circ_matrix_matchpoint.tt2
Open-ILS/web/templates/default/conify/global/config/metabib_field.tt2 [new file with mode: 0644]
Open-ILS/xul/staff_client/Makefile.am
Open-ILS/xul/staff_client/chrome/content/cat/opac.js
Open-ILS/xul/staff_client/chrome/content/main/constants.js
Open-ILS/xul/staff_client/chrome/content/main/menu.js
Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul
Open-ILS/xul/staff_client/chrome/content/util/list.js
Open-ILS/xul/staff_client/server/cat/copy_browser.js
Open-ILS/xul/staff_client/server/cat/copy_editor.js
Open-ILS/xul/staff_client/server/cat/util.js
Open-ILS/xul/staff_client/server/cat/volume_copy_creator.js
Open-ILS/xul/staff_client/server/cat/volume_editor.js
Open-ILS/xul/staff_client/server/cat/volume_editor.xul
Open-ILS/xul/staff_client/server/circ/alternate_copy_summary.js
Open-ILS/xul/staff_client/server/circ/alternate_copy_summary.xul
Open-ILS/xul/staff_client/server/circ/copy_status.js
Open-ILS/xul/staff_client/server/locale/en-US/cat.properties
Open-ILS/xul/staff_client/server/locale/en-US/circ.properties
Open-ILS/xul/staff_client/server/locale/en-US/serial.properties
Open-ILS/xul/staff_client/server/patron/holds.js
Open-ILS/xul/staff_client/server/patron/search_result.js
Open-ILS/xul/staff_client/server/serial/sdist_editor.js
Open-ILS/xul/staff_client/server/serial/ssub_editor.js
Open-ILS/xul/staff_client/server/skin/patron_display.css
README

index 74fb380..f9b824b 100644 (file)
@@ -1154,6 +1154,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
             <field reporter:label="MARC Form" name="marc_form" oils_persist:primitive="string" reporter:datatype="float"/>
             <field reporter:label="Videorecording Format" name="marc_vr_format" oils_persist:primitive="string" reporter:datatype="float"/>
             <field reporter:label="Reference?" name="ref_flag" reporter:datatype="float"/>
+            <field reporter:label="Item Age &lt;" name="item_age" reporter:datatype="float"/>
         </fields>
         <links/>
         <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
@@ -1182,6 +1183,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
             <field reporter:label="Reference?" name="ref_flag" reporter:datatype="float"/>
             <field reporter:label="User Age: Lower Bound" name="usr_age_lower_bound" reporter:datatype="float"/>
             <field reporter:label="User Age: Upper Bound" name="usr_age_upper_bound" reporter:datatype="float"/>
+            <field reporter:label="Item Age &lt;" name="item_age" reporter:datatype="float"/>
         </fields>
         <links/>
         <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
@@ -1235,6 +1237,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <field reporter:label="MARC Bib Level" name="marc_bib_level" oils_persist:primitive="string" reporter:datatype="link"/>
                        <field reporter:label="Videorecording Format" name="marc_vr_format" oils_persist:primitive="string" reporter:datatype="link"/>
                        <field reporter:label="Reference?" name="ref_flag" reporter:datatype="bool"/>
+            <field reporter:label="Item Age &lt;" name="item_age" reporter:datatype="text"/>
                        <field reporter:label="Holdable?" name="holdable" reporter:datatype="bool"/>
                        <field reporter:label="Range is from Owning Lib?" name="distance_is_from_owner" reporter:datatype="bool"/>
                        <field reporter:label="Transit Range" name="transit_range" reporter:datatype="link"/>
@@ -1287,6 +1290,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
             <field reporter:label="Juvenile?" name="juvenile_flag" reporter:datatype="bool"/>
                        <field reporter:label="User Age: Lower Bound" name="usr_age_lower_bound" reporter:datatype="text"/>
                        <field reporter:label="User Age: Upper Bound" name="usr_age_upper_bound" reporter:datatype="text"/>
+            <field reporter:label="Item Age &lt;" name="item_age" reporter:datatype="text"/>
                        <field reporter:label="Circulate?" name="circulate" reporter:datatype="bool"/>
                        <field reporter:label="Duration Rule" name="duration_rule" reporter:datatype="link"/>
                        <field reporter:label="Recurring Fine Rule" name="recurring_fine_rule" reporter:datatype="link"/>
@@ -2711,6 +2715,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <field name="id" reporter:selector="name" reporter:datatype="id"/>
                        <field name="name"  reporter:datatype="text" oils_persist:i18n="true"/>
                        <field name="opac_visible" reporter:datatype="bool"/>
+            <field name="copy_active" reporter:datatype="bool"/>
                </fields>
                <links/>
         <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
@@ -3789,6 +3794,7 @@ SELECT  usr,
                        <field reporter:label="Can Circulate" name="circulate" reporter:datatype="bool"/>
                        <field reporter:label="Copy Number on Volume" name="copy_number" reporter:datatype="text"/>
                        <field reporter:label="Creation Date/Time" name="create_date" reporter:datatype="timestamp"/>
+                       <field reporter:label="Active Date/Time" name="active_date" reporter:datatype="timestamp"/>
                        <field reporter:label="Creating User" name="creator" reporter:datatype="link"/>
                        <field reporter:label="Is Deleted" name="deleted" reporter:datatype="bool"/>
                        <field reporter:label="Dummy ISBN" name="dummy_isbn" reporter:datatype="text"/>
@@ -4815,6 +4821,7 @@ SELECT  usr,
                        <field reporter:label="Can Circulate" name="circulate" reporter:datatype="bool"/>
                        <field reporter:label="Copy Number on Volume" name="copy_number" reporter:datatype="text"/>
                        <field reporter:label="Creation Date/Time" name="create_date" reporter:datatype="timestamp"/>
+                       <field reporter:label="Active Date/Time" name="active_date" reporter:datatype="timestamp"/>
                        <field reporter:label="Creating User" name="creator" reporter:datatype="link"/>
                        <field reporter:label="Is Deleted" name="deleted" reporter:datatype="bool"/>
                        <field reporter:label="Dummy ISBN" name="dummy_isbn" reporter:datatype="text"/>
index 3111626..8adecf8 100644 (file)
                     -->
                 </checkin_override>
 
+                <checkout_override>
+                    <event>COPY_ALERT_MESSAGE</event>
+                </checkout_override>
+
                 <!-- If uncommented, overrides the legacy_script_support value in opensrf.xml for SIP. -->
                 <!--
                 <legacy_script_support>false</legacy_script_support>
index e41f477..0b16511 100644 (file)
@@ -279,16 +279,28 @@ vim:et:ts=4:sw=4:
 
 
         <added_content>
-
             <!-- load the OpenLibrary added content module -->
             <module>OpenILS::WWW::AddedContent::OpenLibrary</module>
 
             <!--
             Max number of seconds to wait for an added content request to 
             return data.  Data not returned within the timeout is considered
-            a failure
+            a failure.
+
+            Note that the pool of Apache processes used by the AddedContent
+            module is the same pool used by core Evergreen processes such as
+            search, circulation, etc. Therefore, the higher you set this
+            timeout value, the more likely you are to run out of available
+            Apache processes resulting in an accidental (or purposeful) denial
+            of service - particularly if the added content server starts
+            responding abnormally slowly.
+
+            The safest option is to disable the AddedContent module completely,
+            but 3 seconds is a compromise between the threat of a denial of
+            service and the enhanced user experience offered by successful added
+            content requests.
             -->
-            <timeout>30</timeout>
+            <timeout>3</timeout>
 
             <!--
             After added content lookups have been disabled due to too many
index 7d52ad5..e14f4c2 100644 (file)
@@ -150,7 +150,7 @@ EXTRA_DIST = @srcdir@/perlmods @srcdir@/templates @top_srcdir@/Open-ILS/xsl @src
 
 # Install header files
 
-oilsincludedir = $(DESTDIR)@includedir@/openils
+oilsincludedir = @includedir@/openils
 headsdir = @top_srcdir@/Open-ILS/include/openils
 oilsinclude_HEADERS = $(headsdir)/idl_fieldmapper.h $(headsdir)/oils_constants.h $(headsdir)/oils_event.h $(headsdir)/oils_idl.h $(headsdir)/oils_utils.h
 
@@ -167,13 +167,13 @@ uninstall-hook:
 #perl-install and string-templates-install     
 ilscore-install:
        @echo $@
-       $(MKDIR_P) $(TEMPLATEDIR)
-       cp -r @srcdir@/templates/marc $(TEMPLATEDIR)
-       cp -r @srcdir@/templates/password-reset $(TEMPLATEDIR)
-       @echo "Installing string templates to $(TEMPLATEDIR)"
-       $(MKDIR_P) $(TEMPLATEDIR)
-       $(MKDIR_P) $(datadir)/overdue/
-       cp -r @srcdir@/templates/strings $(TEMPLATEDIR)
+       $(MKDIR_P) $(DESTDIR)$(TEMPLATEDIR)
+       cp -r @srcdir@/templates/marc $(DESTDIR)$(TEMPLATEDIR)
+       cp -r @srcdir@/templates/password-reset $(DESTDIR)$(TEMPLATEDIR)
+       @echo "Installing string templates to $(DESTDIR)$(TEMPLATEDIR)"
+       $(MKDIR_P) $(DESTDIR)$(TEMPLATEDIR)
+       $(MKDIR_P) $(DESTDIR)$(datadir)/overdue/
+       cp -r @srcdir@/templates/strings $(DESTDIR)$(TEMPLATEDIR)
        sed -i 's|LOCALSTATEDIR|@localstatedir@|g' '$(DESTDIR)@sysconfdir@/oils_sip.xml.example'
        sed -i 's|SYSCONFDIR|@sysconfdir@|g' '$(DESTDIR)@sysconfdir@/oils_sip.xml.example'
        sed -i 's|LOCALSTATEDIR|@localstatedir@|g' '$(DESTDIR)@sysconfdir@/opensrf_core.xml.example'
index 5360875..dd1f938 100644 (file)
@@ -6,6 +6,7 @@
 
 AM_CFLAGS = -D_LARGEFILE64_SOURCE -Wall -I@abs_top_srcdir@/Open-ILS/include/ -I$(LIBXML2_HEADERS) -I$(APACHE2_HEADERS) -I$(APR_HEADERS) -I$(OPENSRF_HEADERS)
 AM_LDFLAGS = -L$(LIBDIR) -L$(OPENSRF_LIBS)
+AP_LIBEXECDIR = `$(APXS2) -q LIBEXECDIR`
 
 if BUILDAPACHEMODS
 OILSAPACHEINST = apachemods
@@ -14,10 +15,11 @@ endif
 install-exec-local: $(OILSAPACHEINST)
 
 apachemods:
+       $(MKDIR_P) $(DESTDIR)$(AP_LIBEXECDIR)
        $(APXS2) -c $(AM_LDFLAGS) -lxml2 -lopensrf -lxslt -lexpat $(AM_CFLAGS) @srcdir@/mod_xmlent.c
-       $(APXS2) -i -a @srcdir@/mod_xmlent.la
+       $(APXS2) -i -S LIBEXECDIR=$(DESTDIR)$(AP_LIBEXECDIR) -a @srcdir@/mod_xmlent.la
        $(APXS2) -c $(AM_LDFLAGS) -lxml2 -lopensrf -lxslt -lexpat $(AM_CFLAGS) @srcdir@/mod_idlchunk.c
-       $(APXS2) -i -a @srcdir@/mod_idlchunk.la
+       $(APXS2) -i -S LIBEXECDIR=$(DESTDIR)$(AP_LIBEXECDIR) -a @srcdir@/mod_idlchunk.la
 
 clean-local:
        rm -f @srcdir@/mod_xmlent.la @srcdir@/mod_xmlent.lo @srcdir@/mod_xmlent.slo
index 263989a..2a3afa7 100644 (file)
@@ -266,6 +266,7 @@ rhel: install_redhat_pgsql centos_like
 centos_like: install_centos_rpms install_yaz install_cpan_marc install install_centos_perl create_ld_local install_cpan_safe install_cpan_force
 
 fedora14: install_fedora_rpms install_cpan install_cpan_fedora install_cpan_marc install_js_sm install_cpan_force
+fedora15: fedora14
 
 debian-squeeze: squeeze generic_debian
 squeeze: install_pgsql_client_debs_90  install_extra_debs_squeeze
index 0d27bfa..4f49264 100644 (file)
@@ -24,7 +24,7 @@ install: build-perl
        ./Build install
 
 build-perl:
-       perl Build.PL || make -s build-perl-fail
+       perl Build.PL --destdir $(DESTDIR) || make -s build-perl-fail
 
 build-perl-fail:
        echo
index 9126ad8..ec381ab 100644 (file)
@@ -163,6 +163,7 @@ __PACKAGE__->register_method(
        method  => "bucket_retrieve_class",
        api_name        => "open-ils.actor.container.retrieve_by_class",
        argc            => 3, 
+       authoritative   => 1, 
        notes           => <<"  NOTES");
                Retrieves all un-fleshed buckets by class assigned to given user 
                PARAMS(authtoken, bucketOwnerId, class [, type])
index 974390e..8e311f6 100644 (file)
@@ -26,6 +26,7 @@ use OpenSRF::AppSession;
 my $U = "OpenILS::Application::AppUtils";
 my $conf;
 my %marctemplates;
+my $assetcom = 'OpenILS::Application::Cat::AssetCommon';
 
 __PACKAGE__->register_method(
     method   => "retrieve_marc_template",
@@ -868,21 +869,20 @@ sub fleshed_volume_update {
         if( $vol->isdeleted ) {
 
             $logger->info("vol-update: deleting volume");
-            return $editor->event unless
+            return $editor->die_event unless
                 $editor->allowed('UPDATE_VOLUME', $vol->owning_lib);
-            my $cs = $editor->search_asset_copy(
-                { call_number => $vol->id, deleted => 'f' } );
-            return OpenILS::Event->new(
-                'VOLUME_NOT_EMPTY', payload => $vol->id ) if @$cs;
 
-            $vol->deleted('t');
-            return $editor->event unless
+            if(my $evt = $assetcom->delete_volume($editor, $vol, $override, $$options{force_delete_copies})) {
+                $editor->rollback;
+                return $evt;
+            }
+
+            return $editor->die_event unless
                 $editor->update_asset_call_number($vol);
 
-            
         } elsif( $vol->isnew ) {
             $logger->info("vol-update: creating volume");
-            $evt = OpenILS::Application::Cat::AssetCommon->create_volume( $override, $editor, $vol );
+            $evt = $assetcom->create_volume( $override, $editor, $vol );
             return $evt if $evt;
 
         } elsif( $vol->ischanged ) {
@@ -895,7 +895,7 @@ sub fleshed_volume_update {
         # now update any attached copies
         if( $copies and @$copies and !$vol->isdeleted ) {
             $_->call_number($vol->id) for @$copies;
-            $evt = OpenILS::Application::Cat::AssetCommon->update_fleshed_copies(
+            $evt = $assetcom->update_fleshed_copies(
                 $editor, $override, $vol, $copies, $delete_stats, $retarget_holds, undef);
             return $evt if $evt;
         }
index c24a9f3..11ecdfc 100644 (file)
@@ -60,7 +60,7 @@ sub create_copy {
 
        my $evt;
        my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
-       return $evt if ($evt = OpenILS::Application::Cat::AssetCommon->org_cannot_have_vols($editor, $org));
+       return $evt if ($evt = $class->org_cannot_have_vols($editor, $org));
 
        $copy->clear_id;
        $copy->editor($editor->requestor->id);
@@ -196,11 +196,14 @@ sub update_copy {
 
        my $evt;
        my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
-       return $evt if ( $evt = OpenILS::Application::Cat::AssetCommon->org_cannot_have_vols($editor, $org) );
+       return $evt if ( $evt = $class->org_cannot_have_vols($editor, $org) );
 
        $logger->info("vol-update: updating copy ".$copy->id);
        my $orig_copy = $editor->retrieve_asset_copy($copy->id);
-       my $orig_vol  = $editor->retrieve_asset_call_number($copy->call_number);
+
+    # Call-number may have changed, find the original
+    my $orig_vol_id = $editor->json_query({select => {acp => ['call_number']}, from => 'acp', where => {id => $copy->id}});
+    my $orig_vol  = $editor->retrieve_asset_call_number($orig_vol_id->[0]->{call_number});
 
        $copy->editor($editor->requestor->id);
        $copy->edit_date('now');
@@ -313,7 +316,7 @@ sub update_fleshed_copies {
 
 
 sub delete_copy {
-       my($class, $editor, $override, $vol, $copy, $retarget_holds, $force_delete_empty_bib) = @_;
+       my($class, $editor, $override, $vol, $copy, $retarget_holds, $force_delete_empty_bib, $skip_empty_cleanup) = @_;
 
    return $editor->event unless 
       $editor->allowed('DELETE_COPY', $class->copy_perm_org($vol, $copy));
@@ -344,12 +347,73 @@ sub delete_copy {
                        or return $editor->event;
        }
 
+    my $evt = $class->cancel_copy_holds($editor, $copy);
+    return $evt if $evt;
+
     $class->check_hold_retarget($editor, $copy, undef, $retarget_holds);
 
+    return undef if $skip_empty_cleanup;
+
        return $class->remove_empty_objects($editor, $override, $vol, $force_delete_empty_bib);
 }
 
 
+# deletes all holds that specifically target the deleted copy
+sub cancel_copy_holds {
+    my($class, $editor, $copy) = @_;
+
+    my $holds = $editor->search_action_hold_request({   
+        target              => $copy->id, 
+        hold_type           => [qw/C R F/],
+        cancel_time         => undef, 
+        fulfillment_time    => undef 
+    });
+
+    return $class->cancel_hold_list($editor, $holds);
+}
+
+# deletes all holds that specifically target the deleted volume
+sub cancel_volume_holds {
+    my($class, $editor, $volume) = @_;
+
+    my $holds = $editor->search_action_hold_request({   
+        target              => $volume->id, 
+        hold_type           => 'V',
+        cancel_time         => undef, 
+        fulfillment_time    => undef 
+    });
+
+    return $class->cancel_hold_list($editor, $holds);
+}
+
+sub cancel_hold_list {
+    my($class, $editor, $holds) = @_;
+
+    for my $hold (@$holds) {
+
+        $hold->cancel_time('now');
+        $hold->cancel_cause(1); # un-targeted expiration.  Do we need an alternate "target deleted" cause?
+        $editor->update_action_hold_request($hold) or return $editor->die_event;
+
+        # delete the copy maps.  
+        my $maps = $editor->search_action_hold_copy_map({hold => $hold->id});
+        for(@$maps) {
+            $editor->delete_action_hold_copy_map($_) 
+                or return $editor->die_event;
+        }
+
+        # tell A/T the hold was cancelled.  Don't wait for a response..
+        my $at_ses = OpenSRF::AppSession->create('open-ils.trigger');
+        $at_ses->request(
+            'open-ils.trigger.event.autocreate',
+            'hold_request.cancel.expire_no_target', 
+            $hold, $hold->pickup_lib);
+    }
+
+    return undef;
+}
+
+
 
 sub create_volume {
        my($class, $override, $editor, $vol) = @_;
@@ -447,7 +511,7 @@ sub find_or_create_volume {
        $vol->suffix($suffix);
        $vol->record($record_id);
 
-    my $evt = OpenILS::Application::Cat::AssetCommon->create_volume(0, $e, $vol);
+    my $evt = $class->create_volume(0, $e, $vol);
     return (undef, $evt) if $evt;
 
        return ($vol);
@@ -479,10 +543,8 @@ sub remove_empty_objects {
 
         # delete this volume if it's not already marked as deleted
         unless( $U->is_true($vol->deleted) || $vol->isdeleted ) {
-            $vol->deleted('t');
-            $vol->editor($editor->requestor->id);
-            $vol->edit_date('now');
-            $editor->update_asset_call_number($vol) or return $editor->event;
+            my $evt = $class->delete_volume($editor, $vol, $override, 0, 1);
+            return $evt if $evt;
         }
 
         return OpenILS::Event->new('TITLE_LAST_COPY', payload => $vol->record ) 
@@ -493,11 +555,65 @@ sub remove_empty_objects {
             my $evt = OpenILS::Application::Cat::BibCommon->delete_rec($editor, $vol->record);
             return $evt if $evt;
         }
-       }
+
+       } else {
+
+        # this may be the last copy attached to the volume.  
+
+        if($U->ou_ancestor_setting_value(
+                $editor->requestor->ws_ou, 'cat.volume.delete_on_empty', $editor)) {
+
+            # if this volume is "empty" and not mid-delete, delete it.
+            unless($U->is_true($vol->deleted) || $vol->isdeleted) {
+
+                my $copies = $editor->search_asset_copy(
+                    [{call_number => $vol->id, deleted => 'f'}, {limit => 1}], {idlist => 1});
+
+                if(!@$copies) {
+                    my $evt = $class->delete_volume($editor, $vol, $override, 0, 1);
+                    return $evt if $evt;
+                }
+            }
+        }
+    }
 
        return undef;
 }
 
+# Deletes a volume.  Returns undef on success, event on error
+# force : deletes all attached copies
+# skip_copy_check : assumes caller has verified no copies need deleting first
+sub delete_volume {
+    my($class, $editor, $vol, $override, $delete_copies, $skip_copy_checks) = @_;
+    my $evt;
+
+    unless($skip_copy_checks) {
+        my $cs = $editor->search_asset_copy(
+            [{call_number => $vol->id, deleted => 'f'}, {limit => 1}], {idlist => 1});
+
+        return OpenILS::Event->new('VOLUME_NOT_EMPTY', payload => $vol->id) 
+            if @$cs and !$delete_copies;
+
+        my $copies = $editor->search_asset_copy({call_number => $vol->id, deleted => 'f'});
+
+        for my $copy (@$copies) {
+            $evt = $class->delete_copy($editor, $override, $vol, $copy, 0, 0, 1);
+            return $evt if $evt;
+        }
+    }
+
+    $vol->deleted('t');
+    $vol->edit_date('now');
+    $vol->editor($editor->requestor->id);
+    $editor->update_asset_call_number($vol) or return $editor->die_event;
+
+    $evt = $class->cancel_volume_holds($editor, $vol);
+    return $evt if $evt;
+
+    # handle the case where this is the last volume on the record
+       return $class->remove_empty_objects($editor, $override, $vol);
+}
+
 
 sub copy_perm_org {
        my($class, $vol, $copy) = @_;
index 4d928a8..6a776d8 100644 (file)
@@ -340,6 +340,32 @@ sub delete_rec {
 
    $editor->update_biblio_record_entry($rec) or return $editor->event;
 
+    my $holds = $editor->search_action_hold_request({
+        target => $rec->id,
+        hold_type => 'T',
+        cancel_time => undef,
+        fulfillment_time => undef
+    });
+
+    for my $hold (@$holds) {
+
+        $hold->cancel_time('now');
+        $hold->cancel_cause(1); # un-targeted expiration.
+        $editor->update_action_hold_request($hold) or return $editor->die_event;
+
+        my $maps = $editor->search_action_hold_copy_map({hold => $hold->id});
+        for(@$maps) {
+            $editor->delete_action_hold_copy_map($_) 
+                or return $editor->die_event;
+        }
+
+        my $at_ses = OpenSRF::AppSession->create('open-ils.trigger');
+        $at_ses->request(
+            'open-ils.trigger.event.autocreate',
+            'hold_request.cancel.expire_no_target', 
+            $hold, $hold->pickup_lib);
+    }
+
    return undef;
 }
 
index df81b27..f5cc14c 100644 (file)
@@ -1306,79 +1306,100 @@ __PACKAGE__->register_method(
 sub mark_item_missing_pieces {
        my( $self, $conn, $auth, $copy_id, $args ) = @_;
     ### FIXME: We're starting a transaction here, but we're doing a lot of things outside of the transaction
-       my $e = new_editor(authtoken=>$auth, xact =>1);
-       return $e->die_event unless $e->checkauth;
+    ### FIXME: Even better, we're going to use two transactions, the first to affect pertinent holds before checkout can
+
+       my $e2 = new_editor(authtoken=>$auth, xact =>1);
+       return $e2->die_event unless $e2->checkauth;
     $args ||= {};
 
-    my $copy = $e->retrieve_asset_copy([
+    my $copy = $e2->retrieve_asset_copy([
         $copy_id,
         {flesh => 1, flesh_fields => {'acp' => ['call_number']}}])
-            or return $e->die_event;
+            or return $e2->die_event;
 
     my $owning_lib = 
         ($copy->call_number->id == OILS_PRECAT_CALL_NUMBER) ? 
             $copy->circ_lib : $copy->call_number->owning_lib;
 
-    return $e->die_event unless $e->allowed('MARK_ITEM_MISSING_PIECES', $owning_lib);
+    return $e2->die_event unless $e2->allowed('MARK_ITEM_MISSING_PIECES', $owning_lib);
 
     #### grab the last circulation
-    my $circ = $e->search_action_circulation([
+    my $circ = $e2->search_action_circulation([
         {   target_copy => $copy->id}, 
         {   limit => 1, 
             order_by => {circ => "xact_start DESC"}
         }
     ])->[0];
 
-    if ($circ) {
-        if (! $circ->checkin_time) { # if circ active, attempt renew
-            my ($res) = $self->method_lookup('open-ils.circ.renew')->run($e->authtoken,{'copy_id'=>$circ->target_copy});
-            if (ref $res ne 'ARRAY') { $res = [ $res ]; }
-            if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
-                $circ = $res->[0]->{payload}{'circ'};
-                $circ->target_copy( $copy->id );
-                $logger->info('open-ils.circ.mark_item_missing_pieces: successful renewal');
-            } else {
-                $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful renewal');
-            }
-        } else {
+    if (!$circ) {
+        $logger->info('open-ils.circ.mark_item_missing_pieces: no previous checkout');
+        $e2->rollback;
+        return OpenILS::Event->new('ACTION_CIRCULATION_NOT_FOUND',{'copy'=>$copy});
+    }
 
-            my $co_params = {
-                'copy_id'=>$circ->target_copy,
-                'patron_id'=>$circ->usr,
-                'skip_deposit_fee'=>1,
-                'skip_rental_fee'=>1
-            };
+       my $holds = $e2->search_action_hold_request(
+               { 
+                       current_copy => $copy->id,
+                       fulfillment_time => undef,
+                       cancel_time => undef,
+               }
+       );
 
-            if ($U->ou_ancestor_setting_value($e->requestor->ws_ou, 'circ.block_renews_for_holds')) {
+    $logger->debug("resetting holds that target the marked copy");
+    OpenILS::Application::Circ::Holds->_reset_hold($e2->requestor, $_) for @$holds;
 
-                my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
-                    $e, $copy, $e->requestor, 1 );
+    
+       if (! $e2->commit) {
+        return $e2->die_event;
+    }
 
-                if ($hold) { # needed for hold? then due now
+       my $e = new_editor(authtoken=>$auth, xact =>1);
+       return $e->die_event unless $e->checkauth;
 
-                    $logger->info('open-ils.circ.mark_item_missing_pieces: item needed for hold, shortening due date');
-                    my $due_date = DateTime->now(time_zone => 'local');
-                    $co_params->{'due_date'} = cleanse_ISO8601( $due_date->strftime('%FT%T%z') );
-                } else {
-                    $logger->info('open-ils.circ.mark_item_missing_pieces: item not needed for hold');
-                }
-            }
+    if (! $circ->checkin_time) { # if circ active, attempt renew
+        my ($res) = $self->method_lookup('open-ils.circ.renew')->run($e->authtoken,{'copy_id'=>$circ->target_copy});
+        if (ref $res ne 'ARRAY') { $res = [ $res ]; }
+        if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
+            $circ = $res->[0]->{payload}{'circ'};
+            $circ->target_copy( $copy->id );
+            $logger->info('open-ils.circ.mark_item_missing_pieces: successful renewal');
+        } else {
+            $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful renewal');
+        }
+    } else {
+
+        my $co_params = {
+            'copy_id'=>$circ->target_copy,
+            'patron_id'=>$circ->usr,
+            'skip_deposit_fee'=>1,
+            'skip_rental_fee'=>1
+        };
+
+        if ($U->ou_ancestor_setting_value($e->requestor->ws_ou, 'circ.block_renews_for_holds')) {
+
+            my ($hold, undef, $retarget) = $holdcode->find_nearest_permitted_hold(
+                $e, $copy, $e->requestor, 1 );
 
-            my ($res) = $self->method_lookup('open-ils.circ.checkout.full.override')->run($e->authtoken,$co_params);
-            if (ref $res ne 'ARRAY') { $res = [ $res ]; }
-            if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
-                $logger->info('open-ils.circ.mark_item_missing_pieces: successful checkout');
-                $circ = $res->[0]->{payload}{'circ'};
+            if ($hold) { # needed for hold? then due now
+
+                $logger->info('open-ils.circ.mark_item_missing_pieces: item needed for hold, shortening due date');
+                my $due_date = DateTime->now(time_zone => 'local');
+                $co_params->{'due_date'} = cleanse_ISO8601( $due_date->strftime('%FT%T%z') );
             } else {
-                $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful checkout');
-                $e->rollback;
-                return $res;
+                $logger->info('open-ils.circ.mark_item_missing_pieces: item not needed for hold');
             }
         }
-    } else {
-        $logger->info('open-ils.circ.mark_item_missing_pieces: no previous checkout');
-        $e->rollback;
-        return OpenILS::Event->new('ACTION_CIRCULATION_NOT_FOUND',{'copy'=>$copy});
+
+        my ($res) = $self->method_lookup('open-ils.circ.checkout.full.override')->run($e->authtoken,$co_params);
+        if (ref $res ne 'ARRAY') { $res = [ $res ]; }
+        if ( $res->[0]->{textcode} eq 'SUCCESS' ) {
+            $logger->info('open-ils.circ.mark_item_missing_pieces: successful checkout');
+            $circ = $res->[0]->{payload}{'circ'};
+        } else {
+            $logger->info('open-ils.circ.mark_item_missing_pieces: non-successful checkout');
+            $e->rollback;
+            return $res;
+        }
     }
 
     ### Update the item status
@@ -1396,17 +1417,6 @@ sub mark_item_missing_pieces {
 
        $e->update_asset_copy($copy) or return $e->die_event;
 
-       my $holds = $e->search_action_hold_request(
-               { 
-                       current_copy => $copy->id,
-                       fulfillment_time => undef,
-                       cancel_time => undef,
-               }
-       );
-
-    $logger->debug("resetting holds that target the marked copy");
-    OpenILS::Application::Circ::Holds->_reset_hold($e->requestor, $_) for @$holds;
-
        if ($e->commit) {
 
         my $ses = OpenSRF::AppSession->create('open-ils.trigger');
index 4d65617..0b36f40 100644 (file)
@@ -342,20 +342,31 @@ sub run_method {
         $circulator->editor->rollback;
 
     } else {
+
         $circulator->editor->commit;
-    }
 
-    $circulator->script_runner->cleanup if $circulator->script_runner;
+        if ($circulator->generate_lost_overdue) {
+            # Generating additional overdue billings has to happen after the 
+            # main commit and before the final respond() so the caller can
+            # receive the latest transaction summary.
+            my $evt = $circulator->generate_lost_overdue_fines;
+            $circulator->bail_on_events($evt) if $evt;
+        }
+    }
     
     $conn->respond_complete(circ_events($circulator));
 
-    unless($circulator->bail_out) {
-        $circulator->do_hold_notify($circulator->notify_hold)
-            if $circulator->notify_hold;
-        $circulator->retarget_holds if $circulator->retarget;
-        $circulator->append_reading_list;
-        $circulator->make_trigger_events;
-    }
+    $circulator->script_runner->cleanup if $circulator->script_runner;
+
+    return undef if $circulator->bail_out;
+
+    $circulator->do_hold_notify($circulator->notify_hold)
+        if $circulator->notify_hold;
+    $circulator->retarget_holds if $circulator->retarget;
+    $circulator->append_reading_list;
+    $circulator->make_trigger_events;
+    
+    return undef;
 }
 
 sub circ_events {
@@ -522,6 +533,7 @@ my @AUTOLOAD_FIELDS = qw/
     skip_deposit_fee
     skip_rental_fee
     use_booking
+    generate_lost_overdue
 /;
 
 
@@ -3134,10 +3146,12 @@ sub checkin_handle_lost {
             $circ_lib, OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN, $self->editor) || 0;
         my $restore_od = $U->ou_ancestor_setting_value(
             $circ_lib, OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN, $self->editor) || 0;
+        $self->generate_lost_overdue(1) if $U->ou_ancestor_setting_value(
+            $circ_lib, OILS_SETTING_GENERATE_OVERDUE_ON_LOST_RETURN, $self->editor);
 
         $self->checkin_handle_lost_now_found(3) if $void_lost;
         $self->checkin_handle_lost_now_found(4) if $void_lost_fee;
-        $self->checkin_handle_lost_now_found_restore_od() if $restore_od && ! $self->void_overdues;
+        $self->checkin_handle_lost_now_found_restore_od($circ_lib) if $restore_od && ! $self->void_overdues;
     }
 
     $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
@@ -3486,6 +3500,7 @@ sub checkin_handle_lost_now_found {
 
 sub checkin_handle_lost_now_found_restore_od {
     my $self = shift;
+    my $circ_lib = shift;
 
     # ------------------------------------------------------------------
     # restore those overdue charges voided when item was set to lost
@@ -3514,4 +3529,58 @@ sub checkin_handle_lost_now_found_restore_od {
     }
 }
 
+# ------------------------------------------------------------------
+# Lost-then-found item checked in.  This sub generates new overdue
+# fines, beyond the point of any existing and possibly voided 
+# overdue fines, up to the point of final checkin time (or max fine
+# amount).  
+# ------------------------------------------------------------------
+sub generate_lost_overdue_fines {
+    my $self = shift;
+    my $circ = $self->circ;
+    my $e = $self->editor;
+
+    # Re-open the transaction so the fine generator can see it
+    if($circ->xact_finish or $circ->stop_fines) {
+        $e->xact_begin;
+        $circ->clear_xact_finish;
+        $circ->clear_stop_fines;
+        $circ->clear_stop_fines_time;
+        $e->update_action_circulation($circ) or return $e->die_event;
+        $e->xact_commit;
+    }
+
+    $e->xact_begin; # generate_fines expects an in-xact editor
+    $self->generate_fines;
+    $circ = $self->circ; # generate fines re-fetches the circ
+    
+    my $update = 0;
+
+    # Re-close the transaction if no money is owed
+    my ($obt) = $U->fetch_mbts($circ->id, $e);
+    if ($obt and $obt->balance_owed == 0) {
+        $circ->xact_finish('now');
+        $update = 1;
+    }
+
+    # Set stop fines if the fine generator didn't have to
+    unless($circ->stop_fines) {
+        $circ->stop_fines(OILS_STOP_FINES_CHECKIN);
+        $circ->stop_fines_time('now');
+        $update = 1;
+    }
+
+    # update the event data sent to the caller within the transaction
+    $self->checkin_flesh_events;
+
+    if ($update) {
+        $e->update_action_circulation($circ) or return $e->die_event;
+        $e->commit;
+    } else {
+        $e->rollback;
+    }
+
+    return undef;
+}
+
 1;
index 1be6838..6d47aeb 100644 (file)
@@ -3422,6 +3422,22 @@ __PACKAGE__->register_method(
     }
 );
 
+__PACKAGE__->register_method(
+    method    => 'change_hold_title_for_specific_holds',
+    api_name  => 'open-ils.circ.hold.change_title.specific_holds',
+    signature => {
+        desc => q/
+            Updates specified holds to target new bib./,
+        params => [
+            { desc => 'Authentication Token', type => 'string' },
+            { desc => 'New Target Bib Id',    type => 'number' },
+            { desc => 'Holds Ids for holds to update',   type => 'array'  },
+        ],
+        return => { desc => '1 on success' }
+    }
+);
+
+
 sub change_hold_title {
     my( $self, $client, $auth, $new_bib_id, $bib_ids ) = @_;
 
@@ -3453,9 +3469,46 @@ sub change_hold_title {
 
     $e->commit;
 
+    _reset_hold($self, $e->requestor, $_) for @$holds;
+
     return 1;
 }
 
+sub change_hold_title_for_specific_holds {
+    my( $self, $client, $auth, $new_bib_id, $hold_ids ) = @_;
+
+    my $e = new_editor(authtoken=>$auth, xact=>1);
+    return $e->die_event unless $e->checkauth;
+
+    my $holds = $e->search_action_hold_request(
+        [
+            {
+                cancel_time      => undef,
+                fulfillment_time => undef,
+                hold_type        => 'T',
+                id               => $hold_ids
+            },
+            {
+                flesh        => 1,
+                flesh_fields => { ahr => ['usr'] }
+            }
+        ],
+        { substream => 1 }
+    );
+
+    for my $hold (@$holds) {
+        $e->allowed('UPDATE_HOLD', $hold->usr->home_ou) or return $e->die_event;
+        $logger->info("Changing hold " . $hold->id . " target from " . $hold->target . " to $new_bib_id in title hold target change");
+        $hold->target( $new_bib_id );
+        $e->update_action_hold_request($hold) or return $e->die_event;
+    }
+
+    $e->commit;
+
+    _reset_hold($self, $e->requestor, $_) for @$holds;
+
+    return 1;
+}
 
 __PACKAGE__->register_method(
     method    => 'rec_hold_count',
index 11c3202..0617f5e 100644 (file)
@@ -122,7 +122,7 @@ sub bib_to_svr {
        my $mfhd_parser = OpenILS::Utils::MFHDParser->new();
        foreach (@$sdists) {
         my $svr;
-        if (ref $_->record_entry and $_->summary_method ne 'use_sdist_only') {
+        if ($_->summary_method ne 'use_sdist_only' and ref $_->record_entry and !$U->is_true($_->record_entry->deleted)) {
             my $skip_all_computable = 0;
             if ($_->summary_method eq 'merge_with_sre') { # 'computable' (85x/86x combos) are handled by generated_coverage when attempting to merge
                 $skip_all_computable = 1;
@@ -130,7 +130,11 @@ sub bib_to_svr {
             $svr = $mfhd_parser->generate_svr($_->record_entry->id, $_->record_entry->marc, $_->record_entry->owning_lib, $skip_all_computable);
         } else {
             $svr = Fieldmapper::serial::virtual_record->new;
-            $svr->sre_id(-1);
+            if (ref $_->record_entry and !$U->is_true($_->record_entry->deleted)) {
+                $svr->sre_id($_->record_entry->id);
+            } else {
+                $svr->sre_id(-1);
+            }
             $svr->location($_->holding_lib->name);
             $svr->owning_lib($_->holding_lib);
             $svr->basic_holdings([]);
index 6cecb8d..0fc1061 100644 (file)
@@ -72,7 +72,7 @@ __PACKAGE__->columns( Essential => qw/call_number barcode creator create_date ed
                                   fine_level circulate deposit price ref opac_visible
                                   circ_as_type circ_modifier deposit_amount location mint_condition
                                   holdable dummy_title dummy_author deleted alert_message
-                                  age_protect floating cost status_changed_time/ );
+                                  age_protect floating cost status_changed_time active_date/ );
 
 #-------------------------------------------------------------------------------
 package asset::copy_part_map;
index da03f1f..5034730 100644 (file)
@@ -73,7 +73,7 @@ package config::copy_status;
 use base qw/config/;
 __PACKAGE__->table('config_copy_status');
 __PACKAGE__->columns(Primary => 'id');
-__PACKAGE__->columns(Essential => qw/name holdable opac_visible/);
+__PACKAGE__->columns(Essential => qw/name holdable opac_visible copy_active/);
 #-------------------------------------------------------------------------------
 
 package config::net_access_level;
index 431c672..0264aa6 100644 (file)
@@ -45,7 +45,7 @@ __PACKAGE__->columns( Essential => qw/call_number barcode creator create_date ed
                                   fine_level circulate deposit price ref opac_visible dummy_isbn
                                   circ_as_type circ_modifier deposit_amount location mint_condition
                                   holdable dummy_title dummy_author deleted alert_message
-                                  age_protect floating summary_contents detailed_contents/ );
+                                  age_protect floating summary_contents detailed_contents active_date/ );
 
 #-------------------------------------------------------------------------------
 package serial::record_entry;
index 5ded5bd..c9743e2 100644 (file)
@@ -815,7 +815,7 @@ sub generate_fines {
             next unless ($c->fine_interval);
         }
         #TODO: reservation grace periods
-        my $grace_period = ($is_reservation ? 0 : $c->grace_period);
+        my $grace_period = ($is_reservation ? 0 : interval_to_seconds($c->grace_period));
 
                try {
                        if ($self->method_lookup('open-ils.storage.transaction.current')->run) {
index 260af53..98e18cc 100644 (file)
@@ -77,6 +77,8 @@ sub HoldIsAvailable {
         !$hold->cancel_time and
         $hold->capture_time and 
         $hold->current_copy and
+        $hold->shelf_time and
+        !$hold->fulfillment_time and
         $hold->current_copy->status == OILS_COPY_STATUS_ON_HOLDS_SHELF;
 
     return 0;
index 6da2ed6..f86a801 100644 (file)
@@ -91,6 +91,7 @@ econst OILS_SETTING_VOID_LOST_PROCESS_FEE_ON_CHECKIN    => 'circ.void_lost_proc_
 econst OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN      => 'circ.restore_overdue_on_lost_return';
 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';
 
 
 
index 1b94492..bede75d 100644 (file)
@@ -25,122 +25,135 @@ my %fields = (
              security_inhibit => 0,
              due              => undef,
              renew_ok         => 0,
-             );
+             );
 
 sub new {
-        my $class = shift;
+    my $class = shift;
 
     my $self = $class->SUPER::new(@_);
 
     my $element;
 
-       foreach $element (keys %fields) {
-               $self->{_permitted}->{$element} = $fields{$element};
-       }
+    foreach $element (keys %fields) {
+        $self->{_permitted}->{$element} = $fields{$element};
+    }
 
     @{$self}{keys %fields} = values %fields;
-        
+
+    $self->load_override_events;
+
     return bless $self, $class;
 }
 
+# Lifted from Checkin.pm to load the list of events that we'll try to
+# override if they occur during checkout or renewal.
+my %override_events;
+sub load_override_events {
+    return if %override_events;
+    my $override = OpenILS::SIP->config->{implementation_config}->{checkout_override};
+    return unless $override;
+    my $events = $override->{event};
+    $events = [$events] unless ref $events eq 'ARRAY';
+    $override_events{$_} = 1 for @$events;
+}
 
 # if this item is already checked out to the requested patron,
-# renew the item and set $self->renew_ok to true.  
-# XXX if it's a renewal and the renewal is not permitted, set 
+# renew the item and set $self->renew_ok to true.
+# XXX if it's a renewal and the renewal is not permitted, set
 # $self->screen_msg("Item on Hold for Another User"); (or somesuch)
 # XXX Set $self->ok(0) on any errors
 sub do_checkout {
-       my $self = shift;
-       my $is_renew = shift || 0;
+    my $self = shift;
+    my $is_renew = shift || 0;
 
-       $self->ok(0); 
+    $self->ok(0);
 
-       my $args = { 
-               barcode => $self->{item}->id, 
+    my $args = {
+               barcode => $self->{item}->id,
                patron_barcode => $self->{patron}->id
-       };
-
-       my $resp;
-
-       if ($is_renew) {
-               $resp = $U->simplereq(
-                       'open-ils.circ',
-                       'open-ils.circ.renew', $self->{authtoken},
-                       { barcode => $self->item->id, patron_barcode => $self->patron->id });
-       } else {
-               $resp = $U->simplereq(
-                       'open-ils.circ',
-                       'open-ils.circ.checkout.permit', 
-                       $self->{authtoken}, $args );
-
-               $resp = [$resp] unless ref $resp eq 'ARRAY';
-
-               my $key;
-
-               syslog('LOG_DEBUG', "OILS: Checkout permit returned event: " . OpenSRF::Utils::JSON->perl2JSON($resp));
-
-               if( @$resp == 1 and ! $U->event_code($$resp[0]) ) {
-                       $key = $$resp[0]->{payload};
-                       syslog('LOG_INFO', "OILS: circ permit key => $key");
-
-               } else {
-
-                       # We got one or more non-success events
-                       $self->screen_msg('');
-                       for my $r (@$resp) {
-
-                               if( my $code = $U->event_code($resp) ) {
-                                       my $txt = $resp->{textcode};
-                                       syslog('LOG_INFO', "OILS: Checkout permit failed with event $code : $txt");
-
-                                       if( $txt eq 'OPEN_CIRCULATION_EXISTS' ) {
-                                               $self->screen_msg(OILS_SIP_MSG_CIRC_EXISTS);
-                                               return 0;
-                                       } else {
-                                               $self->screen_msg(OILS_SIP_MSG_CIRC_PERMIT_FAILED);
-                                       }
-                               }
-                       }
-                       return 0;
-               }
-
-               # --------------------------------------------------------------------
-               # Now do the actual checkout
-               # --------------------------------------------------------------------
-
-               $args = { 
-                       permit_key              => $key, 
-                       patron_barcode => $self->{patron}->id, 
-                       barcode                 => $self->{item}->id
-               };
-
-               $resp = $U->simplereq(
-                       'open-ils.circ',
-                       'open-ils.circ.checkout', $self->{authtoken}, $args );
-       }
-
-       syslog('LOG_INFO', "OILS: Checkout returned event: " . OpenSRF::Utils::JSON->perl2JSON($resp));
-
-       # XXX Check for events
-       if( $resp ) {
-
-               if( my $code = $U->event_code($resp) ) {
-                       my $txt = $resp->{textcode};
-                       syslog('LOG_INFO', "OILS: Checkout failed with event $code : $txt");
-                       $self->screen_msg('Checkout failed.  Please contact a librarian');
-                       return 0; 
-               }
-
-               syslog('LOG_INFO', "OILS: Checkout succeeded");
-
-               my $circ = $resp->{payload}->{circ};
-               $self->{'due'} = OpenILS::SIP->format_date($circ->due_date, 'due');
-               $self->ok(1);
-
-               return 1;
-       }
-
-       return 0;
+               };
+
+    my ($resp, $method);
+
+    my $override = 0;
+
+    while (1) {
+        if ($is_renew) {
+            $method = 'open-ils.circ.renew';
+            $method .= '.override' if ($override);
+            $resp = $U->simplereq('open-ils.circ', $method, $self->{authtoken}, $args);
+        } else {
+            $method = 'open-ils.circ.checkout.permit';
+            $method .= '.override' if ($override);
+            $resp = $U->simplereq('open-ils.circ', $method, $self->{authtoken}, $args);
+
+            $resp = [$resp] unless ref $resp eq 'ARRAY';
+
+            syslog('LOG_DEBUG', "OILS: $method returned event: " . OpenSRF::Utils::JSON->perl2JSON($resp));
+
+            if (@$resp == 1 && !$U->event_code($$resp[0])) {
+                my $key = $$resp[0]->{payload};
+                syslog('LOG_INFO', "OILS: circ permit key => $key");
+                # --------------------------------------------------------------------
+                # Now do the actual checkout
+                # --------------------------------------------------------------------
+                my $cko_args = $args;
+                $cko_args->{permit_key} = $key;
+                $method = 'open-ils.circ.checkout';
+                $resp = $U->simplereq('open-ils.circ', $method, $self->{authtoken}, $cko_args);
+            } else {
+                # We got one or more non-success events
+                $self->screen_msg('');
+                for my $r (@$resp) {
+                    if ( my $code = $U->event_code($r) ) {
+                        my $txt = $r->{textcode};
+                        syslog('LOG_INFO', "OILS: $method failed with event $code : $txt");
+
+                        if ($override_events{$txt} && $method !~ /override$/) {
+                            # Found an event we've been configured to override.
+                            $override = 1;
+                        } elsif ( $txt eq 'OPEN_CIRCULATION_EXISTS' ) {
+                            $self->screen_msg(OILS_SIP_MSG_CIRC_EXISTS);
+                            return 0;
+                        } else {
+                            $self->screen_msg(OILS_SIP_MSG_CIRC_PERMIT_FAILED);
+                            return 0;
+                        }
+                    }
+                }
+                # This looks potentially dangerous, but we shouldn't
+                # end up here if the loop iterated with $override = 1;
+                next if ($override && $method !~ /override$/);
+            }
+        }
+        syslog('LOG_INFO', "OILS: $method returned event: " . OpenSRF::Utils::JSON->perl2JSON($resp));
+        # XXX Check for events
+        if ( $resp ) {
+
+            if ( my $code = $U->event_code($resp) ) {
+                my $txt = $resp->{textcode};
+                if ($override_events{$txt} && $method !~ /override$/) {
+                    $override = 1;
+                } else {
+                    syslog('LOG_INFO', "OILS: $method failed with event $code : $txt");
+                    $self->screen_msg('Checkout failed.  Please contact a librarian');
+                    last;
+                }
+            } else {
+                syslog('LOG_INFO', "OILS: $method succeeded");
+
+                my $circ = $resp->{payload}->{circ};
+                $self->{'due'} = OpenILS::SIP->format_date($circ->due_date, 'due');
+                $self->ok(1);
+                last;
+            }
+
+        }
+        last if ($method =~ /override$/);
+    }
+
+
+    return $self->ok;
 }
 
 
index 48f00cb..5b728c0 100644 (file)
@@ -357,6 +357,8 @@ sub get_compressed_holdings {
         @decomp_holdings = $self->get_decompressed_holdings($caption, {'dedupe' => 1});
     }
 
+    return () if !@decomp_holdings;
+
     my $runner = $decomp_holdings[0]->clone->increment;   
     my $curr_holding = shift(@decomp_holdings);
     $curr_holding = $curr_holding->clone;
@@ -414,6 +416,9 @@ sub get_decompressed_holdings {
     my $link_id = $caption->link_id;
     $htag =~ s/^85/86/;
     my @holdings = $self->holdings($htag, $link_id);
+
+    return () if !@holdings;
+
     my @decomp_holdings;
 
     foreach my $holding (@holdings) {
index 47a561a..39bfaeb 100644 (file)
@@ -162,13 +162,19 @@ sub check_age_protect {
                { order_by => 'age' }
        );
 
-       # Now, now many seconds old is this copy
-       my $create_date = DateTime::Format::ISO8601
-               ->new
-               ->parse_datetime( OpenSRF::Utils::cleanse_ISO8601($copy->create_date) )
-               ->epoch;
-
-       my $age = time - $create_date;
+    my $age_protect_date = $copy->create_date;
+    $age_protect_date = $copy->active_date if($U->ou_ancestor_setting_value($copy->circ_lib, 'circ.holds.age_protect.active_date'));
+
+    my $age = 0;
+    my $age_protect_parsed;
+    if($age_protect_date) {
+       # Now, now many seconds old is this copy
+           $age_protect_parsed = DateTime::Format::ISO8601
+                   ->new
+               ->parse_datetime( OpenSRF::Utils::cleanse_ISO8601($age_protect_date) )
+               ->epoch;
+           $age = time - $age_protect_parsed;
+    }
 
        for my $protection ( @$protection_list ) {
 
@@ -180,7 +186,7 @@ sub check_age_protect {
                # How many seconds old does the copy have to be to escape age protection
                my $interval = OpenSRF::Utils::interval_to_seconds($protection->age);
 
-               $logger->info("age_protect interval=$interval, create_date=$create_date, age=$age");
+               $logger->info("age_protect interval=$interval, age_protect_date=$age_protect_parsed, age=$age");
 
                if( $interval > $age ) { 
                        # if age of the item is less than the protection interval, 
index dd7382e..e9c6546 100755 (executable)
@@ -101,4 +101,33 @@ sub parse {
     return $self;
 }
 
+sub toURI {
+    my $class = shift;
+    my $parts = shift || {};
+
+    my $self = ref($class) ? $class : $class->new;
+
+    $self->$_($$parts{$_}) for keys %$parts;
+    return undef unless (defined($self->classname) && defined($self->id));
+
+    my $tag = 'tag:';
+
+    if ($self->host) {
+        $tag .= $self->host;
+        $tag .= ',' . $self->validity if ($self->validity);
+    }
+
+    $tag .= ':';
+
+    $tag .= 'U2@' if ($self->version == 2);
+    $tag .= $self->classname . '/' . $self->id;
+    $tag .= '['. join(',', @{ $self->paging }) . ']' if defined($self->paging);
+    $tag .= '{'. join(',', @{ $self->includes }) . '}' if defined($self->includes);
+    $tag .= '/' . $self->location if defined($self->location);
+    $tag .= '/' . $self->depth if defined($self->depth);
+    $tag .= '/' . $self->pathinfo if defined($self->pathinfo);
+
+    return $tag;
+}
 
+1;
index 2be38a0..c2c2cc6 100644 (file)
@@ -28,16 +28,13 @@ use Data::Dumper;
 
 my $AC = 'OpenILS::WWW::AddedContent';
 
-# These URLs are always the same for OpenLibrary, so there's no advantage to
-# pulling from opensrf.xml; we hardcode them here
+# This should work for most setups
+my $blank_img = 'http://localhost/opac/images/blank.png';
 
-# jscmd=details is unstable but includes goodies such as Table of Contents
-my $base_url_details = 'http://openlibrary.org/api/books?format=json&jscmd=details&bibkeys=ISBN:';
+# This URL is always the same for OpenLibrary, so there's no advantage to
+# pulling from opensrf.xml
 
-# jscmd=data is stable and contains links to ebooks, excerpts, etc
-my $base_url_data = 'http://openlibrary.org/api/books?format=json&jscmd=data&bibkeys=ISBN:';
-
-my $cover_base_url = 'http://covers.openlibrary.org/b/isbn/';
+my $read_api = 'http://openlibrary.org/api/volumes/brief/json/';
 
 sub new {
     my( $class, $args ) = @_;
@@ -68,7 +65,7 @@ sub jacket_large {
 
 sub ebooks_html {
     my( $self, $key ) = @_;
-    my $book_data_json = $self->fetch_data_response($key)->content();
+    my $book_data_json = $self->fetch_response($key);
 
     $logger->debug("$key: " . $book_data_json);
 
@@ -125,27 +122,17 @@ sub ebooks_html {
 
 sub excerpt_html {
     my( $self, $key ) = @_;
-    my $book_details_json = $self->fetch_details_response($key)->content();
-
-    $logger->debug("$key: $book_details_json");
 
     my $excerpt_html;
-    
-    my $book_details = OpenSRF::Utils::JSON->JSON2perl($book_details_json);
-    my $book_key = (keys %$book_details)[0];
 
-    # We didn't find a matching book; short-circuit our response
-    if (!$book_key) {
-        $logger->debug("$key: no found book");
-        return 0;
-    }
+    my $content = $self->fetch_details_response($key)->content();
 
-    my $first_sentence = $book_details->{$book_key}->{first_sentence};
+    my $first_sentence = $content->{first_sentence};
     if ($first_sentence) {
         $excerpt_html .= "<div class='sentence1'>$first_sentence</div>\n";
     }
 
-    my $excerpts_json = $book_details->{$book_key}->{excerpts};
+    my $excerpts_json = $content->{excerpts};
     if ($excerpts_json && scalar(@$excerpts_json)) {
         # Load up excerpt text with comments in tooltip
         foreach my $excerpt (@$excerpts_json) {
@@ -177,22 +164,12 @@ HTML table.
 
 sub toc_html {
     my( $self, $key ) = @_;
-    my $book_details_json = $self->fetch_details_response($key)->content();
-
-    $logger->debug("$key: " . $book_details_json);
 
     my $toc_html;
     
-    my $book_details = OpenSRF::Utils::JSON->JSON2perl($book_details_json);
-    my $book_key = (keys %$book_details)[0];
+    my $book_data = $self->fetch_data_response($key) || return 0;
 
-    # We didn't find a matching book; short-circuit our response
-    if (!$book_key) {
-        $logger->debug("$key: no found book");
-        return 0;
-    }
-
-    my $toc_json = $book_details->{$book_key}->{details}->{table_of_contents};
+    my $toc_json = $book_data->{table_of_contents};
 
     # No table of contents is available for this book; short-circuit
     if (!$toc_json or !scalar(@$toc_json)) {
@@ -204,7 +181,7 @@ sub toc_html {
     # and page number. Some rows may not contain section numbers, we should
     # protect against empty page numbers too.
     foreach my $chapter (@$toc_json) {
-       my $label = $chapter->{label};
+        my $label = $chapter->{label};
         if ($label) {
             $label .= '. ';
         }
@@ -225,7 +202,7 @@ sub toc_html {
 sub toc_json {
     my( $self, $key ) = @_;
     my $toc = $self->send_json(
-        $self->fetch_details_response($key)
+        $self->fetch_response($key)
     );
 }
 
@@ -264,29 +241,70 @@ sub send_html {
     return { content_type => 'text/html', content => $HTML };
 }
 
-# returns the HTTP response object from the URL fetch
-sub fetch_data_response {
+# proxy OpenLibrary requests so that the IP address of the library
+# can be used to determine access rights to materials
+sub proxy_json {
     my( $self, $key ) = @_;
-    my $url = $base_url_data . "$key";
-    my $response = $AC->get_url($url);
-    return $response;
+
+    my $url = $read_api . $key;
+    $logger->debug("proxy_json with key '$key', url $url");
+
+    $self->send_json($AC->get_url($url)->content());
 }
+
+
 # returns the HTTP response object from the URL fetch
-sub fetch_details_response {
+sub fetch_response {
     my( $self, $key ) = @_;
-    my $url = $base_url_details . "$key";
-    my $response = $AC->get_url($url);
-    return $response;
+
+    # TODO: OpenLibrary can also accept lccn, oclc, olid...
+    # Hardcoded to only accept ISBNs for now.
+    $key = "isbn:$key";
+
+    my $url = $read_api . $key;
+    my $response = $AC->get_url($url)->content();
+
+    $logger->debug("$key: response was $response");
+
+    my $book_results = OpenSRF::Utils::JSON->JSON2perl($response);
+    my $record = $book_results->{$key};
+
+    # We didn't find a matching book; short-circuit our response
+    if (!$record) {
+        $logger->debug("$key: no found record");
+        return 0;
+    }
+
+    return $record;
 }
 
-# returns the HTTP response object from the URL fetch
-sub fetch_cover_response {
-    my( $self, $size, $key ) = @_;
+sub fetch_data_response {
+    my ($self, $key) = @_;
 
-    my $response = $self->fetch_data_response($key)->content();
+    my $book_results = $self->fetch_response($key);
 
-    my $book_data = OpenSRF::Utils::JSON->JSON2perl($response);
-    my $book_key = (keys %$book_data)[0];
+    my $book_key = (keys %{$book_results->{records}})[0];
+
+    $logger->debug("$key: using record key $book_key");
+
+    # We didn't find a matching book; short-circuit our response
+    if (!$book_key || !$book_results->{records}->{$book_key}->{data}) {
+        $logger->debug("$key: no found book");
+        return 0;
+    }
+
+    return $book_results->{records}->{$book_key}->{data};
+}
+
+
+sub fetch_details_response {
+    my ($self, $key) = @_;
+
+    my $book_results = $self->fetch_response($key);
+
+    my $book_key = (keys %{$book_results->{records}})[0];
+
+    $logger->debug("$key: using record key $book_key");
 
     # We didn't find a matching book; short-circuit our response
     if (!$book_key) {
@@ -294,15 +312,67 @@ sub fetch_cover_response {
         return 0;
     }
 
-    my $covers_json = $book_data->{$book_key}->{cover};
-    if (!$covers_json) {
-        $logger->debug("$key: no covers for this book");
+    return $book_results->{$book_key}->{details};
+}
+
+sub fetch_items_response {
+    my ($self, $key) = @_;
+
+    my $book_results = $self->fetch_response($key) || return 0;
+
+    my $items = $book_results->{items};
+
+    # We didn't find a matching book; short-circuit our response
+    if (!$items || scalar(@$items) == 0) {
+        $logger->debug("$key: no found items");
         return 0;
     }
 
-    $logger->debug("$key: " . $covers_json->{$size});
-    return $AC->get_url($covers_json->{$size}) || 0;
+    return $book_results->{items};
 }
 
+# returns a cover image from the list of associated items
+# TODO: Look for the best match in the array of items
+sub fetch_cover_response {
+    my( $self, $size, $key ) = @_;
+
+    my $cover;
+
+    my $response = $self->fetch_response($key);
+
+    # Short-circuit if we get an empty response, or a response
+    #with no matching records
+    if (!$response or scalar(keys %$response) == 0) {
+        return $AC->get_url($blank_img);
+    }
+
+    # Try to return a cover image from the record->data metadata
+    foreach my $rec_key (keys %{$response->{records}}) {
+        my $record = $response->{records}->{$rec_key};
+        if (exists $record->{data}->{cover}->{$size}) {
+            $cover = $record->{data}->{cover}->{$size};
+        }
+        if ($cover) {
+            return $AC->get_url($cover);
+        }
+    }
+
+    # If we didn't find a cover in the record metadata, look in the items
+    # Seems unlikely, but might as well try.
+    my $items = $response->{items};
+
+    $logger->debug("$key: items request got " . scalar(@$items) . " items back");
+
+    foreach my $item (@$items) {
+        if (exists $item->{cover}->{$size}) {
+            return $AC->get_url($item->{cover}->{$size}) || 0;
+        }
+    }
+
+    $logger->debug("$key: no covers for this book");
+
+    # Return a blank image
+    return $AC->get_url($blank_img);
+}
 
 1;
index 18cf327..56af440 100644 (file)
@@ -364,7 +364,7 @@ sub show_template {
             var bucketStore = new dojo.data.ItemFileReadStore(
                 { data : cbreb.toStoreData(
                         fieldmapper.standardRequest(
-                            ['open-ils.actor','open-ils.actor.container.retrieve_by_class'],
+                            ['open-ils.actor','open-ils.actor.container.retrieve_by_class.authoritative'],
                             [u.authtoken, u.user.id(), 'biblio', 'staff_client']
                         )
                     )
index 70f988d..f24e980 100644 (file)
@@ -1,6 +1,6 @@
 #!perl -T
 
-use Test::More tests => 17;
+use Test::More tests => 19;
 
 use_ok( 'OpenILS::Utils::Cronscript' );
 use_ok( 'OpenILS::Utils::CStoreEditor' );
@@ -19,3 +19,20 @@ use_ok( 'OpenILS::Utils::RemoteAccount' );
 use_ok( 'OpenILS::Utils::ScriptRunner' );
 use_ok( 'OpenILS::Utils::SpiderMonkey' );
 use_ok( 'OpenILS::Utils::ZClient' );
+
+# LP 800269 - Test MFHD holdings for records that only contain a caption field
+my $co_marc = MARC::Record->new();
+$co_marc->append_fields(
+    MARC::Field->new('853','','',
+        '8' => '1',
+        'a' => 'v.',
+        'b' => '[no.]',
+    )
+);
+my $co_mfhd = MFHD->new($co_marc);
+
+my @comp_holdings = $co_mfhd->get_compressed_holdings($co_mfhd->field('853'));
+is(@comp_holdings, 0, "Compressed holdings for an MFHD record that only has a caption");
+
+my @decomp_holdings = $co_mfhd->get_decompressed_holdings($co_mfhd->field('853'));
+is(@decomp_holdings, 0, "Decompressed holdings for an MFHD record that only has a caption");
index e87520c..c7b404d 100644 (file)
@@ -86,7 +86,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 ('0559', :eg_version); -- dbs via miker
+INSERT INTO config.upgrade_log (version, applied_to) VALUES ('0568', :eg_version); -- miker/tsbere
 
 CREATE TABLE config.bib_source (
        id              SERIAL  PRIMARY KEY,
@@ -349,7 +349,8 @@ CREATE TABLE config.copy_status (
        id              SERIAL  PRIMARY KEY,
        name            TEXT    NOT NULL UNIQUE,
        holdable        BOOL    NOT NULL DEFAULT FALSE,
-       opac_visible    BOOL    NOT NULL DEFAULT FALSE
+       opac_visible    BOOL    NOT NULL DEFAULT FALSE,
+    copy_active  BOOL    NOT NULL DEFAULT FALSE
 );
 COMMENT ON TABLE config.copy_status IS $$
 Copy Statuses
index 010c3bc..ddb116a 100644 (file)
@@ -80,6 +80,7 @@ CREATE TABLE asset.copy (
        floating                BOOL                            NOT NULL DEFAULT FALSE,
        dummy_isbn      TEXT,
        status_changed_time TIMESTAMP WITH TIME ZONE,
+       active_date TIMESTAMP WITH TIME ZONE,
        mint_condition      BOOL        NOT NULL DEFAULT TRUE,
     cost    NUMERIC(8,2)
 );
@@ -118,6 +119,23 @@ RETURNS TRIGGER AS $$
 BEGIN
     IF NEW.status <> OLD.status THEN
         NEW.status_changed_time := now();
+        IF NEW.active_date IS NULL AND NEW.status IN (SELECT id FROM config.copy_status WHERE copy_active = true) THEN
+            NEW.active_date := now();
+        END IF;
+    END IF;
+    RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+-- Need to check on initial create. Fast adds, manual edit of status at create, etc.
+CREATE OR REPLACE FUNCTION asset.acp_created()
+RETURNS TRIGGER AS $$
+BEGIN
+    IF NEW.active_date IS NULL AND NEW.status IN (SELECT id FROM config.copy_status WHERE copy_active = true) THEN
+        NEW.active_date := now();
+    END IF;
+    IF NEW.status_changed_time IS NULL THEN
+        NEW.status_changed_time := now();
     END IF;
     RETURN NEW;
 END;
@@ -127,6 +145,10 @@ CREATE TRIGGER acp_status_changed_trig
     BEFORE UPDATE ON asset.copy
     FOR EACH ROW EXECUTE PROCEDURE asset.acp_status_changed();
 
+CREATE TRIGGER acp_created_trig
+    BEFORE INSERT ON asset.copy
+    FOR EACH ROW EXECUTE PROCEDURE asset.acp_created();
+
 CREATE TABLE asset.stat_cat_sip_fields (
     field   CHAR(2) PRIMARY KEY,
     name    TEXT    NOT NULL,
index febf569..5854d3e 100644 (file)
@@ -19,7 +19,8 @@ CREATE TABLE config.circ_matrix_weights (
     juvenile_flag           NUMERIC(6,2)   NOT NULL,
     is_renewal              NUMERIC(6,2)   NOT NULL,
     usr_age_lower_bound     NUMERIC(6,2)   NOT NULL,
-    usr_age_upper_bound     NUMERIC(6,2)   NOT NULL
+    usr_age_upper_bound     NUMERIC(6,2)   NOT NULL,
+    item_age                NUMERIC(6,2)   NOT NULL
 );
 
 -- Hold Matrix Weights
@@ -39,7 +40,8 @@ CREATE TABLE config.hold_matrix_weights (
     marc_bib_level          NUMERIC(6,2)   NOT NULL,
     marc_vr_format          NUMERIC(6,2)   NOT NULL,
     juvenile_flag           NUMERIC(6,2)   NOT NULL,
-    ref_flag                NUMERIC(6,2)   NOT NULL
+    ref_flag                NUMERIC(6,2)   NOT NULL,
+    item_age                NUMERIC(6,2)   NOT NULL
 );
 
 -- Linking between weights and org units
index fadc392..77e975e 100644 (file)
@@ -69,6 +69,7 @@ CREATE TABLE config.circ_matrix_matchpoint (
     is_renewal           BOOL,
     usr_age_lower_bound  INTERVAL,
     usr_age_upper_bound  INTERVAL,
+    item_age             INTERVAL,
     -- "Result" Fields
     circulate            BOOL,   -- Hard "can't circ" flag requiring an override
     duration_rule        INT     REFERENCES config.rule_circ_duration (id) DEFERRABLE INITIALLY DEFERRED,
@@ -83,7 +84,7 @@ CREATE TABLE config.circ_matrix_matchpoint (
 );
 
 -- Nulls don't count for a constraint match, so we have to coalesce them into something that does.
-CREATE UNIQUE INDEX ccmm_once_per_paramset ON config.circ_matrix_matchpoint (org_unit, grp, COALESCE(circ_modifier, ''), COALESCE(marc_type, ''), COALESCE(marc_form, ''), COALESCE(marc_bib_level,''), COALESCE(marc_vr_format, ''), COALESCE(copy_circ_lib::TEXT, ''), COALESCE(copy_owning_lib::TEXT, ''), COALESCE(user_home_ou::TEXT, ''), COALESCE(ref_flag::TEXT, ''), COALESCE(juvenile_flag::TEXT, ''), COALESCE(is_renewal::TEXT, ''), COALESCE(usr_age_lower_bound::TEXT, ''), COALESCE(usr_age_upper_bound::TEXT, '')) WHERE active;
+CREATE UNIQUE INDEX ccmm_once_per_paramset ON config.circ_matrix_matchpoint (org_unit, grp, COALESCE(circ_modifier, ''), COALESCE(marc_type, ''), COALESCE(marc_form, ''), COALESCE(marc_bib_level,''), COALESCE(marc_vr_format, ''), COALESCE(copy_circ_lib::TEXT, ''), COALESCE(copy_owning_lib::TEXT, ''), COALESCE(user_home_ou::TEXT, ''), COALESCE(ref_flag::TEXT, ''), COALESCE(juvenile_flag::TEXT, ''), COALESCE(is_renewal::TEXT, ''), COALESCE(usr_age_lower_bound::TEXT, ''), COALESCE(usr_age_upper_bound::TEXT, ''), COALESCE(item_age::TEXT, '')) WHERE active;
 
 -- Tests for max items out by circ_modifier
 CREATE TABLE config.circ_matrix_circ_mod_test (
@@ -109,6 +110,7 @@ DECLARE
     matchpoint      config.circ_matrix_matchpoint%ROWTYPE;
     weights         config.circ_matrix_weights%ROWTYPE;
     user_age        INTERVAL;
+    my_item_age     INTERVAL;
     denominator     NUMERIC(6,2);
     row_list        INT[];
     result          action.found_circ_matrix_matchpoint;
@@ -125,6 +127,9 @@ BEGIN
         SELECT INTO user_age age(user_object.dob);
     END IF;
 
+    -- Ditto
+    SELECT INTO my_item_age age(coalesce(item_object.active_date, now()));
+
     -- Grab the closest set circ weight setting.
     SELECT INTO weights cw.*
       FROM config.weight_assoc wa
@@ -151,6 +156,7 @@ BEGIN
         weights.is_renewal          := 7.0;
         weights.usr_age_lower_bound := 0.0;
         weights.usr_age_upper_bound := 0.0;
+        weights.item_age            := 0.0;
     END IF;
 
     -- Determine the max (expected) depth (+1) of the org tree and max depth of the permisson tree
@@ -194,6 +200,7 @@ BEGIN
                 AND (m.marc_bib_level           IS NULL OR m.marc_bib_level = rec_descriptor.bib_level)
                 AND (m.marc_vr_format           IS NULL OR m.marc_vr_format = rec_descriptor.vr_format)
                 AND (m.ref_flag                 IS NULL OR m.ref_flag = item_object.ref)
+                AND (m.item_age                 IS NULL OR (my_item_age IS NOT NULL AND m.item_age > my_item_age))
           ORDER BY
                 -- Permission Groups
                 CASE WHEN upgad.distance        IS NOT NULL THEN 2^(2*weights.grp - (upgad.distance/denominator)) ELSE 0.0 END +
@@ -213,7 +220,11 @@ BEGIN
                 CASE WHEN m.marc_type           IS NOT NULL THEN 4^weights.marc_type ELSE 0.0 END +
                 CASE WHEN m.marc_form           IS NOT NULL THEN 4^weights.marc_form ELSE 0.0 END +
                 CASE WHEN m.marc_vr_format      IS NOT NULL THEN 4^weights.marc_vr_format ELSE 0.0 END +
-                CASE WHEN m.ref_flag            IS NOT NULL THEN 4^weights.ref_flag ELSE 0.0 END DESC,
+                CASE WHEN m.ref_flag            IS NOT NULL THEN 4^weights.ref_flag ELSE 0.0 END +
+                -- Item age has a slight adjustment to weight based on value.
+                -- This should ensure that a shorter age limit comes first when all else is equal.
+                -- NOTE: This assumes that intervals will normally be in days.
+                CASE WHEN m.item_age            IS NOT NULL THEN 4^weights.item_age - 1 + 86400/EXTRACT(EPOCH FROM m.item_age) ELSE 0.0 END DESC,
                 -- Final sort on id, so that if two rules have the same sorting in the previous sort they have a defined order
                 -- This prevents "we changed the table order by updating a rule, and we started getting different results"
                 m.id LOOP
index 5ffa88f..0bb95de 100644 (file)
@@ -47,6 +47,7 @@ CREATE TABLE config.hold_matrix_matchpoint (
     marc_vr_format          TEXT,
     juvenile_flag           BOOL,
     ref_flag                BOOL,
+    item_age                INTERVAL,
     -- "Result" Fields
     holdable                BOOL    NOT NULL DEFAULT TRUE,                -- Hard "can't hold" flag requiring an override
     distance_is_from_owner  BOOL    NOT NULL DEFAULT FALSE,                -- How to calculate transit_range.  True means owning lib, false means copy circ lib
@@ -58,7 +59,7 @@ CREATE TABLE config.hold_matrix_matchpoint (
 );
 
 -- Nulls don't count for a constraint match, so we have to coalesce them into something that does.
-CREATE UNIQUE INDEX chmm_once_per_paramset ON config.hold_matrix_matchpoint (COALESCE(user_home_ou::TEXT, ''), COALESCE(request_ou::TEXT, ''), COALESCE(pickup_ou::TEXT, ''), COALESCE(item_owning_ou::TEXT, ''), COALESCE(item_circ_ou::TEXT, ''), COALESCE(usr_grp::TEXT, ''), COALESCE(requestor_grp::TEXT, ''), COALESCE(circ_modifier, ''), COALESCE(marc_type, ''), COALESCE(marc_form, ''), COALESCE(marc_bib_level, ''), COALESCE(marc_vr_format, ''), COALESCE(juvenile_flag::TEXT, ''), COALESCE(ref_flag::TEXT, '')) WHERE active;
+CREATE UNIQUE INDEX chmm_once_per_paramset ON config.hold_matrix_matchpoint (COALESCE(user_home_ou::TEXT, ''), COALESCE(request_ou::TEXT, ''), COALESCE(pickup_ou::TEXT, ''), COALESCE(item_owning_ou::TEXT, ''), COALESCE(item_circ_ou::TEXT, ''), COALESCE(usr_grp::TEXT, ''), COALESCE(requestor_grp::TEXT, ''), COALESCE(circ_modifier, ''), COALESCE(marc_type, ''), COALESCE(marc_form, ''), COALESCE(marc_bib_level, ''), COALESCE(marc_vr_format, ''), COALESCE(juvenile_flag::TEXT, ''), COALESCE(ref_flag::TEXT, ''), COALESCE(item_age::TEXT, '')) WHERE active;
 
 CREATE OR REPLACE FUNCTION action.find_hold_matrix_matchpoint(pickup_ou integer, request_ou integer, match_item bigint, match_user integer, match_requestor integer)
   RETURNS integer AS
@@ -68,6 +69,7 @@ DECLARE
     user_object         actor.usr%ROWTYPE;
     item_object         asset.copy%ROWTYPE;
     item_cn_object      asset.call_number%ROWTYPE;
+    my_item_age         INTERVAL;
     rec_descriptor      metabib.rec_descriptor%ROWTYPE;
     matchpoint          config.hold_matrix_matchpoint%ROWTYPE;
     weights             config.hold_matrix_weights%ROWTYPE;
@@ -79,6 +81,8 @@ BEGIN
     SELECT INTO item_cn_object      * FROM asset.call_number        WHERE id = item_object.call_number;
     SELECT INTO rec_descriptor      * FROM metabib.rec_descriptor   WHERE record = item_cn_object.record;
 
+    SELECT INTO my_item_age age(coalesce(item_object.active_date, now()));
+
     -- The item's owner should probably be the one determining if the item is holdable
     -- How to decide that is debatable. Decided to default to the circ library (where the item lives)
     -- This flag will allow for setting it to the owning library (where the call number "lives")
@@ -99,7 +103,7 @@ BEGIN
         SELECT INTO weights hw.*
           FROM config.weight_assoc wa
                JOIN config.hold_matrix_weights hw ON (hw.id = wa.hold_weights)
-               JOIN actor.org_unit_ancestors_distance( cn_object.owning_lib ) d ON (wa.org_unit = d.id)
+               JOIN actor.org_unit_ancestors_distance( item_cn_object.owning_lib ) d ON (wa.org_unit = d.id)
           WHERE active
           ORDER BY d.distance
           LIMIT 1;
@@ -121,6 +125,7 @@ BEGIN
         weights.marc_vr_format  := 1.0;
         weights.juvenile_flag   := 4.0;
         weights.ref_flag        := 0.0;
+        weights.item_age        := 0.0;
     END IF;
 
     -- Determine the max (expected) depth (+1) of the org tree and max depth of the permisson tree
@@ -176,6 +181,7 @@ BEGIN
             AND (m.marc_bib_level       IS NULL OR m.marc_bib_level = rec_descriptor.bib_level)
             AND (m.marc_vr_format       IS NULL OR m.marc_vr_format = rec_descriptor.vr_format)
             AND (m.ref_flag             IS NULL OR m.ref_flag = item_object.ref)
+            AND (m.item_age             IS NULL OR (my_item_age IS NOT NULL AND m.item_age > my_item_age))
       ORDER BY
             -- Permission Groups
             CASE WHEN rpgad.distance    IS NOT NULL THEN 2^(2*weights.requestor_grp - (rpgad.distance/denominator)) ELSE 0.0 END +
@@ -193,7 +199,11 @@ BEGIN
             CASE WHEN m.marc_type       IS NOT NULL THEN 4^weights.marc_type ELSE 0.0 END +
             CASE WHEN m.marc_form       IS NOT NULL THEN 4^weights.marc_form ELSE 0.0 END +
             CASE WHEN m.marc_vr_format  IS NOT NULL THEN 4^weights.marc_vr_format ELSE 0.0 END +
-            CASE WHEN m.ref_flag        IS NOT NULL THEN 4^weights.ref_flag ELSE 0.0 END DESC,
+            CASE WHEN m.ref_flag        IS NOT NULL THEN 4^weights.ref_flag ELSE 0.0 END +
+            -- Item age has a slight adjustment to weight based on value.
+            -- This should ensure that a shorter age limit comes first when all else is equal.
+            -- NOTE: This assumes that intervals will normally be in days.
+            CASE WHEN m.item_age            IS NOT NULL THEN 4^weights.item_age - 86400/EXTRACT(EPOCH FROM m.item_age) ELSE 0.0 END DESC,
             -- Final sort on id, so that if two rules have the same sorting in the previous sort they have a defined order
             -- This prevents "we changed the table order by updating a rule, and we started getting different results"
             m.id;
@@ -217,6 +227,8 @@ DECLARE
     ou_skip              actor.org_unit_setting%ROWTYPE;
     result            action.matrix_test_result;
     hold_test        config.hold_matrix_matchpoint%ROWTYPE;
+    use_active_date   TEXT;
+    age_protect_date  TIMESTAMP WITH TIME ZONE;
     hold_count        INT;
     hold_transit_prox    INT;
     frozen_hold_count    INT;
@@ -357,8 +369,17 @@ BEGIN
 
     IF item_object.age_protect IS NOT NULL THEN
         SELECT INTO age_protect_object * FROM config.rule_age_hold_protect WHERE id = item_object.age_protect;
-
-        IF item_object.create_date + age_protect_object.age > NOW() THEN
+        IF hold_test.distance_is_from_owner THEN
+            SELECT INTO use_active_date value FROM actor.org_unit_ancestor_setting('circ.holds.age_protect.active_date', item_cn_object.owning_lib);
+        ELSE
+            SELECT INTO use_active_date value FROM actor.org_unit_ancestor_setting('circ.holds.age_protect.active_date', item_object.circ_lib);
+        END IF;
+        IF use_active_date = 'true' THEN
+            age_protect_date := COALESCE(item_object.active_date, NOW());
+        ELSE
+            age_protect_date := item_object.create_date;
+        END IF;
+        IF age_protect_date + age_protect_object.age > NOW() THEN
             IF hold_test.distance_is_from_owner THEN
                 SELECT INTO item_cn_object * FROM asset.call_number WHERE id = item_object.call_number;
                 SELECT INTO hold_transit_prox prox FROM actor.org_unit_proximity WHERE from_org = item_cn_object.owning_lib AND to_org = pickup_ou;
index 9b884b6..29617cd 100644 (file)
@@ -230,6 +230,11 @@ CREATE TRIGGER sunit_status_changed_trig
     BEFORE UPDATE ON serial.unit
     FOR EACH ROW EXECUTE PROCEDURE asset.acp_status_changed();
 
+-- ditto
+CREATE TRIGGER sunit_created_trig
+    BEFORE INSERT ON serial.unit
+    FOR EACH ROW EXECUTE PROCEDURE asset.acp_created();
+
 CREATE TABLE serial.item (
        id              SERIAL  PRIMARY KEY,
        creator         INT     NOT NULL
index 35651d2..279f2a4 100644 (file)
@@ -215,22 +215,22 @@ INSERT INTO config.rule_age_hold_protect VALUES
        (2, oils_i18n_gettext(2, '6month', 'crahp', 'name'), '6 months', 2);
 SELECT SETVAL('config.rule_age_hold_protect_id_seq'::TEXT, 100);
 
-INSERT INTO config.copy_status (id,name,holdable,opac_visible) VALUES (0,oils_i18n_gettext(0, 'Available', 'ccs', 'name'),'t','t');
-INSERT INTO config.copy_status (id,name,holdable,opac_visible) VALUES (1,oils_i18n_gettext(1, 'Checked out', 'ccs', 'name'),'t','t');
+INSERT INTO config.copy_status (id,name,holdable,opac_visible,copy_active) VALUES (0,oils_i18n_gettext(0, 'Available', 'ccs', 'name'),'t','t','t');
+INSERT INTO config.copy_status (id,name,holdable,opac_visible,copy_active) VALUES (1,oils_i18n_gettext(1, 'Checked out', 'ccs', 'name'),'t','t','t');
 INSERT INTO config.copy_status (id,name) VALUES (2,oils_i18n_gettext(2, 'Bindery', 'ccs', 'name'));
 INSERT INTO config.copy_status (id,name) VALUES (3,oils_i18n_gettext(3, 'Lost', 'ccs', 'name'));
 INSERT INTO config.copy_status (id,name) VALUES (4,oils_i18n_gettext(4, 'Missing', 'ccs', 'name'));
 INSERT INTO config.copy_status (id,name,holdable,opac_visible) VALUES (5,oils_i18n_gettext(5, 'In process', 'ccs', 'name'),'t','t');
 INSERT INTO config.copy_status (id,name,holdable,opac_visible) VALUES (6,oils_i18n_gettext(6, 'In transit', 'ccs', 'name'),'t','t');
-INSERT INTO config.copy_status (id,name,holdable,opac_visible) VALUES (7,oils_i18n_gettext(7, 'Reshelving', 'ccs', 'name'),'t','t');
-INSERT INTO config.copy_status (id,name,holdable,opac_visible) VALUES (8,oils_i18n_gettext(8, 'On holds shelf', 'ccs', 'name'),'t','t');
+INSERT INTO config.copy_status (id,name,holdable,opac_visible,copy_active) VALUES (7,oils_i18n_gettext(7, 'Reshelving', 'ccs', 'name'),'t','t','t');
+INSERT INTO config.copy_status (id,name,holdable,opac_visible,copy_active) VALUES (8,oils_i18n_gettext(8, 'On holds shelf', 'ccs', 'name'),'t','t','t');
 INSERT INTO config.copy_status (id,name,holdable,opac_visible) VALUES (9,oils_i18n_gettext(9, 'On order', 'ccs', 'name'),'t','t');
-INSERT INTO config.copy_status (id,name) VALUES (10,oils_i18n_gettext(10, 'ILL', 'ccs', 'name'));
+INSERT INTO config.copy_status (id,name,copy_active) VALUES (10,oils_i18n_gettext(10, 'ILL', 'ccs', 'name'),'t');
 INSERT INTO config.copy_status (id,name) VALUES (11,oils_i18n_gettext(11, 'Cataloging', 'ccs', 'name'));
-INSERT INTO config.copy_status (id,name,opac_visible) VALUES (12,oils_i18n_gettext(12, 'Reserves', 'ccs', 'name'),'t');
+INSERT INTO config.copy_status (id,name,opac_visible,copy_active) VALUES (12,oils_i18n_gettext(12, 'Reserves', 'ccs', 'name'),'t','t');
 INSERT INTO config.copy_status (id,name) VALUES (13,oils_i18n_gettext(13, 'Discard/Weed', 'ccs', 'name'));
 INSERT INTO config.copy_status (id,name) VALUES (14,oils_i18n_gettext(14, 'Damaged', 'ccs', 'name'));
-INSERT INTO config.copy_status (id,name) VALUES (15,oils_i18n_gettext(15, 'On reservation shelf', 'ccs', 'name'));
+INSERT INTO config.copy_status (id,name,copy_active) VALUES (15,oils_i18n_gettext(15, 'On reservation shelf', 'ccs', 'name'),'t');
 
 SELECT SETVAL('config.copy_status_id_seq'::TEXT, 100);
 
@@ -1428,7 +1428,9 @@ INSERT INTO permission.perm_list ( id, code, description ) VALUES
  ( 508, 'ABORT_TRANSIT_ON_MISSING', oils_i18n_gettext(508,
     'Allows a user to abort a transit on a copy with status of MISSING', 'ppl', 'description')),
  ( 509, 'TRANSIT_CHECKIN_INTERVAL_BLOCK.override', oils_i18n_gettext(509,
-    'Allows a user to override the TRANSIT_CHECKIN_INTERVAL_BLOCK event', 'ppl', 'description'));
+    'Allows a user to override the TRANSIT_CHECKIN_INTERVAL_BLOCK event', 'ppl', 'description')),
+ ( 510, 'UPDATE_PATRON_COLLECTIONS_EXEMPT', oils_i18n_gettext(510,
+    'Allows a user to indicate that a patron is exempt from collections processing', 'ppl', 'description'));
 
 
 SELECT SETVAL('permission.perm_list_id_seq'::TEXT, 1000);
@@ -1913,6 +1915,7 @@ INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
                        'VIEW_CIRC_MATRIX_MATCHPOINT',
             'ABORT_TRANSIT_ON_LOST', 
             'ABORT_TRANSIT_ON_MISSING',
+            'UPDATE_PATRON_COLLECTIONS_EXEMPT',
                        'VIEW_HOLD_MATRIX_MATCHPOINT');
 
 INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable)
@@ -2278,20 +2281,20 @@ INSERT INTO asset.call_number VALUES (-1,1,NOW(),1,NOW(),-1,1,'UNCATALOGED');
 -- circ matrix
 INSERT INTO config.circ_matrix_matchpoint (org_unit,grp,circulate,duration_rule,recurring_fine_rule,max_fine_rule) VALUES (1,1,true,11,1,1);
 
-INSERT INTO config.circ_matrix_weights(name, org_unit, grp, circ_modifier, marc_type, marc_form, marc_bib_level, marc_vr_format, copy_circ_lib, copy_owning_lib, user_home_ou, ref_flag, juvenile_flag, is_renewal, usr_age_upper_bound, usr_age_lower_bound) VALUES 
-    ('Default', 10.0, 11.0, 5.0, 4.0, 3.0, 2.0, 2.0, 8.0, 8.0, 8.0, 1.0, 6.0, 7.0, 0.0, 0.0),
-    ('Org_Unit_First', 11.0, 10.0, 5.0, 4.0, 3.0, 2.0, 2.0, 8.0, 8.0, 8.0, 1.0, 6.0, 7.0, 0.0, 0.0),
-    ('Item_Owner_First', 8.0, 8.0, 5.0, 4.0, 3.0, 2.0, 2.0, 10.0, 11.0, 8.0, 1.0, 6.0, 7.0, 0.0, 0.0),
-    ('All_Equal', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
+INSERT INTO config.circ_matrix_weights(name, org_unit, grp, circ_modifier, marc_type, marc_form, marc_bib_level, marc_vr_format, copy_circ_lib, copy_owning_lib, user_home_ou, ref_flag, juvenile_flag, is_renewal, usr_age_upper_bound, usr_age_lower_bound, item_age) VALUES 
+    ('Default', 10.0, 11.0, 5.0, 4.0, 3.0, 2.0, 2.0, 8.0, 8.0, 8.0, 1.0, 6.0, 7.0, 0.0, 0.0, 0.0),
+    ('Org_Unit_First', 11.0, 10.0, 5.0, 4.0, 3.0, 2.0, 2.0, 8.0, 8.0, 8.0, 1.0, 6.0, 7.0, 0.0, 0.0, 0.0),
+    ('Item_Owner_First', 8.0, 8.0, 5.0, 4.0, 3.0, 2.0, 2.0, 10.0, 11.0, 8.0, 1.0, 6.0, 7.0, 0.0, 0.0, 0.0),
+    ('All_Equal', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
 
 -- hold matrix - 110.hold_matrix.sql:
 INSERT INTO config.hold_matrix_matchpoint (requestor_grp) VALUES (1);
 
-INSERT INTO config.hold_matrix_weights(name, user_home_ou, request_ou, pickup_ou, item_owning_ou, item_circ_ou, usr_grp, requestor_grp, circ_modifier, marc_type, marc_form, marc_bib_level, marc_vr_format, juvenile_flag, ref_flag) VALUES
-    ('Default', 5.0, 5.0, 5.0, 5.0, 5.0, 7.0, 8.0, 4.0, 3.0, 2.0, 1.0, 1.0, 4.0, 0.0),
-    ('Item_Owner_First', 5.0, 5.0, 5.0, 8.0, 7.0, 5.0, 5.0, 4.0, 3.0, 2.0, 1.0, 1.0, 4.0, 0.0),
-    ('User_Before_Requestor', 5.0, 5.0, 5.0, 5.0, 5.0, 8.0, 7.0, 4.0, 3.0, 2.0, 1.0, 1.0, 4.0, 0.0),
-    ('All_Equal', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
+INSERT INTO config.hold_matrix_weights(name, user_home_ou, request_ou, pickup_ou, item_owning_ou, item_circ_ou, usr_grp, requestor_grp, circ_modifier, marc_type, marc_form, marc_bib_level, marc_vr_format, juvenile_flag, ref_flag, item_age) VALUES
+    ('Default', 5.0, 5.0, 5.0, 5.0, 5.0, 7.0, 8.0, 4.0, 3.0, 2.0, 1.0, 1.0, 4.0, 0.0, 0.0),
+    ('Item_Owner_First', 5.0, 5.0, 5.0, 8.0, 7.0, 5.0, 5.0, 4.0, 3.0, 2.0, 1.0, 1.0, 4.0, 0.0, 0.0),
+    ('User_Before_Requestor', 5.0, 5.0, 5.0, 5.0, 5.0, 8.0, 7.0, 4.0, 3.0, 2.0, 1.0, 1.0, 4.0, 0.0, 0.0),
+    ('All_Equal', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
 
 -- dynamic weight associations
 INSERT INTO config.weight_assoc(active, org_unit, circ_weights, hold_weights) VALUES
@@ -2427,6 +2430,11 @@ INSERT into config.org_unit_setting_type
     oils_i18n_gettext('circ.holds.min_estimated_wait_interval', 'When predicting the amount of time a patron will be waiting for a hold to be fulfilled, this is the minimum estimated length of time to assume an item will be checked out. Examples: "2 weeks", "5 days"', 'coust', 'description'),
     'interval'),
 
+( 'circ.holds.age_protect.active_date',
+    oils_i18n_gettext('circ.holds.age_protect.active_date', 'Holds: Use Active Date for Age Protection', 'coust', 'label'),
+    oils_i18n_gettext('circ.holds.age_protect.active_date', 'When calculating age protection rules use the active date instead of the creation date.', 'coust', 'description'),
+    'bool'),
+
 ( 'circ.selfcheck.patron_login_timeout',
     oils_i18n_gettext('circ.selfcheck.patron_login_timeout', 'Selfcheck: Patron Login Timeout (in seconds)', 'coust', 'label'),
     oils_i18n_gettext('circ.selfcheck.patron_login_timeout', 'Number of seconds of inactivity before the patron is logged out of the selfcheck interface', 'coust', 'description'),
@@ -8740,3 +8748,59 @@ INSERT INTO config.org_unit_setting_type ( name, label, description, datatype )
     'interval'
 );
 
+INSERT INTO config.org_unit_setting_type 
+( name, label, description, datatype ) VALUES 
+( 'cat.volume.delete_on_empty',
+  oils_i18n_gettext('cat.volume.delete_on_empty', 'Cat: Delete volume with last copy', 'coust', 'label'),
+  oils_i18n_gettext('cat.volume.delete_on_empty', 'Automatically delete a volume when the last linked copy is deleted', 'coust', 'description'),
+  'bool'
+);
+
+-- Event def for email notice for hold cancelled due to lack of target -----
+
+INSERT INTO action_trigger.event_definition (id, active, owner, name, hook, validator, reactor, delay, delay_field, group_field, template)
+    VALUES (38, FALSE, 1, 
+        'Hold Cancelled (No Target) Email Notification', 
+        'hold_request.cancel.expire_no_target', 
+        'HoldIsCancelled', 'SendEmail', '30 minutes', 'cancel_time', 'usr',
+$$
+[%- USE date -%]
+[%- user = target.0.usr -%]
+To: [%- params.recipient_email || user.email %]
+From: [%- params.sender_email || default_sender %]
+Subject: Hold Request Cancelled
+
+Dear [% user.family_name %], [% user.first_given_name %]
+The following holds were cancelled because no items were found to fullfil the hold.
+
+[% FOR hold IN target %]
+    Title: [% hold.bib_rec.bib_record.simple_record.title %]
+    Author: [% hold.bib_rec.bib_record.simple_record.author %]
+    Library: [% hold.pickup_lib.name %]
+    Request Date: [% date.format(helpers.format_date(hold.rrequest_time), '%Y-%m-%d') %]
+[% END %]
+
+$$);
+
+INSERT INTO action_trigger.environment (event_def, path) VALUES
+    (38, 'usr'),
+    (38, 'pickup_lib'),
+    (38, 'bib_rec.bib_record.simple_record');
+
+
+INSERT INTO config.org_unit_setting_type ( name, label, description, datatype ) VALUES (
+    'circ.lost.generate_overdue_on_checkin',
+    oils_i18n_gettext( 
+        'circ.lost.generate_overdue_on_checkin',
+        'Circ:  Lost Checkin Generates New Overdues',
+        'coust',
+        'label'
+    ),
+    oils_i18n_gettext( 
+        'circ.lost.generate_overdue_on_checkin',
+        'Enabling this setting causes retroactive creation of not-yet-existing overdue fines on lost item checkin, up to the point of checkin time (or max fines is reached).  This is different than "restore overdue on lost", because it only creates new overdue fines.  Use both settings together to get the full complement of overdue fines for a lost item',
+        'coust',
+        'label'
+    ),
+    'bool'
+);
index c12a3a8..3e85f5a 100644 (file)
@@ -232,7 +232,7 @@ CREATE OR REPLACE FUNCTION unapi.holdings_xml (bid BIGINT, ouid INT, org TEXT, d
                  name holdings,
                  XMLATTRIBUTES(
                     CASE WHEN $8 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
-                    CASE WHEN ('bre' = ANY ('{acn,auri}'::TEXT[] || $5)) THEN 'tag:open-ils.org:U2@bre/' || $1 || '/' || $3 ELSE NULL END AS id
+                    CASE WHEN ('bre' = ANY ($5)) THEN 'tag:open-ils.org:U2@bre/' || $1 || '/' || $3 ELSE NULL END AS id
                  ),
                  XMLELEMENT(
                      name counts,
@@ -253,40 +253,43 @@ CREATE OR REPLACE FUNCTION unapi.holdings_xml (bid BIGINT, ouid INT, org TEXT, d
                  ),
                  CASE 
                      WHEN ('bmp' = ANY ($5)) THEN
-                        XMLELEMENT( name monograph_parts,
-                            XMLAGG((SELECT unapi.bmp( id, 'xml', 'monograph_part', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($5,'bre'), 'holdings_xml'), $3, $4, $6, $7, FALSE) FROM biblio.monograph_part WHERE record = $1))
+                        XMLELEMENT(
+                            name monograph_parts,
+                            (SELECT XMLAGG(bmp) FROM (
+                                SELECT  unapi.bmp( id, 'xml', 'monograph_part', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($5,'bre'), 'holdings_xml'), $3, $4, $6, $7, FALSE)
+                                  FROM  biblio.monograph_part
+                                  WHERE record = $1
+                            )x)
                         )
                      ELSE NULL
                  END,
-                 CASE WHEN ('acn' = ANY ('{acn,auri}'::TEXT[] || $5)) THEN 
-                     XMLELEMENT(
-                         name volumes,
-                         (SELECT XMLAGG(acn) FROM (
-                            SELECT  unapi.acn(acn.id,'xml','volume',array_remove_item_by_value( evergreen.array_remove_item_by_value('{acn,auri}'::TEXT[] || $5,'holdings_xml'),'bre'), $3, $4, $6, $7, FALSE)
-                              FROM  asset.call_number acn
-                              WHERE acn.record = $1
-                                    AND EXISTS (
-                                        SELECT  1
-                                          FROM  asset.copy acp
-                                                JOIN actor.org_unit_descendants(
-                                                    $2,
-                                                    (COALESCE(
-                                                        $4,
-                                                        (SELECT aout.depth
-                                                          FROM  actor.org_unit_type aout
-                                                                JOIN actor.org_unit aou ON (aou.ou_type = aout.id AND aou.id = $2)
-                                                        )
-                                                    ))
-                                                ) aoud ON (acp.circ_lib = aoud.id)
-                                          LIMIT 1
-                                    )
-                              ORDER BY label_sortkey
-                              LIMIT $6
-                              OFFSET $7
-                         )x)
-                     )
-                 ELSE NULL END,
-                 CASE WHEN ('ssub' = ANY ('{acn,auri}'::TEXT[] || $5)) THEN 
+                 XMLELEMENT(
+                     name volumes,
+                     (SELECT XMLAGG(acn) FROM (
+                        SELECT  unapi.acn(acn.id,'xml','volume',array_remove_item_by_value( evergreen.array_remove_item_by_value($5,'holdings_xml'),'bre'), $3, $4, $6, $7, FALSE)
+                          FROM  asset.call_number acn
+                          WHERE acn.record = $1
+                                AND EXISTS (
+                                    SELECT  1
+                                      FROM  asset.copy acp
+                                            JOIN actor.org_unit_descendants(
+                                                $2,
+                                                (COALESCE(
+                                                    $4,
+                                                    (SELECT aout.depth
+                                                      FROM  actor.org_unit_type aout
+                                                            JOIN actor.org_unit aou ON (aou.ou_type = aout.id AND aou.id = $2)
+                                                    )
+                                                ))
+                                            ) aoud ON (acp.circ_lib = aoud.id)
+                                      LIMIT 1
+                               )
+                          ORDER BY label_sortkey
+                          LIMIT $6
+                          OFFSET $7
+                     )x)
+                 ),
+                 CASE WHEN ('ssub' = ANY ($5)) THEN 
                      XMLELEMENT(
                          name subscriptions,
                          (SELECT XMLAGG(ssub) FROM (
@@ -322,7 +325,11 @@ CREATE OR REPLACE FUNCTION unapi.ssub ( obj_id BIGINT, format TEXT,  ename TEXT,
                     XMLELEMENT( name distributions,
                         CASE 
                             WHEN ('sdist' = ANY ($4)) THEN
-                                XMLAGG((SELECT unapi.sdist( id, 'xml', 'distribution', evergreen.array_remove_item_by_value($4,'ssub'), $5, $6, $7, $8, FALSE) FROM serial.distribution WHERE subscription = ssub.id))
+                                (SELECT XMLAGG(sdist) FROM (
+                                    SELECT  unapi.sdist( id, 'xml', 'distribution', evergreen.array_remove_item_by_value($4,'ssub'), $5, $6, $7, $8, FALSE)
+                                      FROM  serial.distribution
+                                      WHERE subscription = ssub.id
+                                )x)
                             ELSE NULL
                         END
                     )
@@ -347,24 +354,40 @@ CREATE OR REPLACE FUNCTION unapi.sdist ( obj_id BIGINT, format TEXT,  ename TEXT
                     XMLELEMENT( name streams,
                         CASE 
                             WHEN ('sstr' = ANY ($4)) THEN
-                                XMLAGG((SELECT unapi.sstr( id, 'xml', 'stream', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE) FROM serial.stream WHERE distribution = sdist.id))
+                                (SELECT XMLAGG(sstr) FROM (
+                                    SELECT  unapi.sstr( id, 'xml', 'stream', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE)
+                                      FROM  serial.stream
+                                      WHERE distribution = sdist.id
+                                )x)
                             ELSE NULL
                         END
                     ),
                     XMLELEMENT( name summaries,
                         CASE 
                             WHEN ('ssum' = ANY ($4)) THEN
-                                XMLAGG((SELECT unapi.sbsum( id, 'xml', 'serial_summary', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE) FROM serial.basic_summary WHERE distribution = sdist.id))
+                                (SELECT XMLAGG(sbsum) FROM (
+                                    SELECT  unapi.sbsum( id, 'xml', 'serial_summary', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE)
+                                      FROM  serial.basic_summary
+                                      WHERE distribution = sdist.id
+                                )x)
                             ELSE NULL
                         END,
                         CASE 
                             WHEN ('ssum' = ANY ($4)) THEN
-                                XMLAGG((SELECT unapi.sisum( id, 'xml', 'serial_summary', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE) FROM serial.index_summary WHERE distribution = sdist.id))
+                                (SELECT XMLAGG(sisum) FROM (
+                                    SELECT  unapi.sisum( id, 'xml', 'serial_summary', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE)
+                                      FROM  serial.index_summary
+                                      WHERE distribution = sdist.id
+                                )x)
                             ELSE NULL
                         END,
                         CASE 
                             WHEN ('ssum' = ANY ($4)) THEN
-                                XMLAGG((SELECT unapi.sssum( id, 'xml', 'serial_summary', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE) FROM serial.supplement_summary WHERE distribution = sdist.id))
+                                (SELECT XMLAGG(sssum) FROM (
+                                    SELECT  unapi.sssum( id, 'xml', 'serial_summary', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE)
+                                      FROM  serial.supplement_summary
+                                      WHERE distribution = sdist.id
+                                )x)
                             ELSE NULL
                         END
                     )
@@ -386,7 +409,11 @@ CREATE OR REPLACE FUNCTION unapi.sstr ( obj_id BIGINT, format TEXT,  ename TEXT,
                 XMLELEMENT( name items,
                     CASE 
                         WHEN ('sitem' = ANY ($4)) THEN
-                            XMLAGG((SELECT unapi.sitem( id, 'xml', 'serial_item', evergreen.array_remove_item_by_value($4,'sstr'), $5, $6, $7, $8, FALSE) FROM serial.item WHERE stream = sstr.id))
+                            (SELECT XMLAGG(sitem) FROM (
+                                SELECT  unapi.sitem( id, 'xml', 'serial_item', evergreen.array_remove_item_by_value($4,'sstr'), $5, $6, $7, $8, FALSE)
+                                  FROM  serial.item
+                                  WHERE stream = sstr.id
+                            )x)
                         ELSE NULL
                     END
                 )
@@ -409,7 +436,11 @@ CREATE OR REPLACE FUNCTION unapi.siss ( obj_id BIGINT, format TEXT,  ename TEXT,
                 XMLELEMENT( name items,
                     CASE 
                         WHEN ('sitem' = ANY ($4)) THEN
-                            XMLAGG((SELECT unapi.sitem( id, 'xml', 'serial_item', evergreen.array_remove_item_by_value($4,'siss'), $5, $6, $7, $8, FALSE) FROM serial.item WHERE issuance = sstr.id))
+                            (SELECT XMLAGG(sitem) FROM (
+                                SELECT  unapi.sitem( id, 'xml', 'serial_item', evergreen.array_remove_item_by_value($4,'siss'), $5, $6, $7, $8, FALSE)
+                                  FROM  serial.item
+                                  WHERE issuance = sstr.id
+                            )x)
                         ELSE NULL
                     END
                 )
@@ -435,7 +466,11 @@ CREATE OR REPLACE FUNCTION unapi.sitem ( obj_id BIGINT, format TEXT,  ename TEXT
 --                    XMLELEMENT( name notes,
 --                        CASE 
 --                            WHEN ('acpn' = ANY ($4)) THEN
---                                XMLAGG((SELECT unapi.acpn( id, 'xml', 'copy_note', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8) FROM asset.copy_note WHERE owning_copy = cp.id AND pub))
+--                                (SELECT XMLAGG(acpn) FROM (
+--                                    SELECT  unapi.acpn( id, 'xml', 'copy_note', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8)
+--                                      FROM  asset.copy_note
+--                                      WHERE owning_copy = cp.id AND pub
+--                                )x)
 --                            ELSE NULL
 --                        END
 --                    )
@@ -636,21 +671,33 @@ CREATE OR REPLACE FUNCTION unapi.acp ( obj_id BIGINT, format TEXT,  ename TEXT,
                     XMLELEMENT( name copy_notes,
                         CASE 
                             WHEN ('acpn' = ANY ($4)) THEN
-                                XMLAGG((SELECT unapi.acpn( id, 'xml', 'copy_note', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE) FROM asset.copy_note WHERE owning_copy = cp.id AND pub))
+                                (SELECT XMLAGG(acpn) FROM (
+                                    SELECT  unapi.acpn( id, 'xml', 'copy_note', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE)
+                                      FROM  asset.copy_note
+                                      WHERE owning_copy = cp.id AND pub
+                                )x)
                             ELSE NULL
                         END
                     ),
                     XMLELEMENT( name statcats,
                         CASE 
                             WHEN ('ascecm' = ANY ($4)) THEN
-                                XMLAGG((SELECT unapi.ascecm( stat_cat_entry, 'xml', 'statcat', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE) FROM asset.stat_cat_entry_copy_map WHERE owning_copy = cp.id))
+                                (SELECT XMLAGG(ascecm) FROM (
+                                    SELECT  unapi.ascecm( stat_cat_entry, 'xml', 'statcat', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE)
+                                      FROM  asset.stat_cat_entry_copy_map
+                                      WHERE owning_copy = cp.id
+                                )x)
                             ELSE NULL
                         END
                     ),
                     XMLELEMENT( name foreign_records,
                         CASE
                             WHEN ('bre' = ANY ($4)) THEN
-                                XMLAGG((SELECT unapi.bre(peer_record,'marcxml','record','{}'::TEXT[], $5, $6, $7, $8, FALSE) FROM biblio.peer_bib_copy_map WHERE target_copy = cp.id))
+                                (SELECT XMLAGG(bre) FROM (
+                                    SELECT  unapi.bre(peer_record,'marcxml','record','{}'::TEXT[], $5, $6, $7, $8, FALSE)
+                                      FROM  biblio.peer_bib_copy_map
+                                      WHERE target_copy = cp.id
+                                )x)
                             ELSE NULL
                         END
 
@@ -658,7 +705,11 @@ CREATE OR REPLACE FUNCTION unapi.acp ( obj_id BIGINT, format TEXT,  ename TEXT,
                     CASE 
                         WHEN ('bmp' = ANY ($4)) THEN
                             XMLELEMENT( name monograph_parts,
-                                XMLAGG((SELECT unapi.bmp( part, 'xml', 'monograph_part', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE) FROM asset.copy_part_map WHERE target_copy = cp.id))
+                                (SELECT XMLAGG(bmp) FROM (
+                                    SELECT  unapi.bmp( part, 'xml', 'monograph_part', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE)
+                                      FROM  asset.copy_part_map
+                                      WHERE target_copy = cp.id
+                                )x)
                             )
                         ELSE NULL
                     END
@@ -687,17 +738,48 @@ CREATE OR REPLACE FUNCTION unapi.sunit ( obj_id BIGINT, format TEXT,  ename TEXT
                     XMLELEMENT( name copy_notes,
                         CASE 
                             WHEN ('acpn' = ANY ($4)) THEN
-                                XMLAGG((SELECT unapi.acpn( id, 'xml', 'copy_note', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($4,'acp'),'sunit'), $5, $6, $7, $8, FALSE) FROM asset.copy_note WHERE owning_copy = cp.id AND pub))
+                                (SELECT XMLAGG(acpn) FROM (
+                                    SELECT  unapi.acpn( id, 'xml', 'copy_note', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($4,'acp'),'sunit'), $5, $6, $7, $8, FALSE)
+                                      FROM  asset.copy_note
+                                      WHERE owning_copy = cp.id AND pub
+                                )x)
                             ELSE NULL
                         END
                     ),
                     XMLELEMENT( name statcats,
                         CASE 
                             WHEN ('ascecm' = ANY ($4)) THEN
-                                XMLAGG((SELECT unapi.acpn( stat_cat_entry, 'xml', 'statcat', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($4,'acp'),'sunit'), $5, $6, $7, $8, FALSE) FROM asset.stat_cat_entry_copy_map WHERE owning_copy = cp.id))
+                                (SELECT XMLAGG(ascecm) FROM (
+                                    SELECT  unapi.ascecm( stat_cat_entry, 'xml', 'statcat', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE)
+                                      FROM  asset.stat_cat_entry_copy_map
+                                      WHERE owning_copy = cp.id
+                                )x)
                             ELSE NULL
                         END
-                    )
+                    ),
+                    XMLELEMENT( name foreign_records,
+                        CASE
+                            WHEN ('bre' = ANY ($4)) THEN
+                                (SELECT XMLAGG(bre) FROM (
+                                    SELECT  unapi.bre(peer_record,'marcxml','record','{}'::TEXT[], $5, $6, $7, $8, FALSE)
+                                      FROM  biblio.peer_bib_copy_map
+                                      WHERE target_copy = cp.id
+                                )x)
+                            ELSE NULL
+                        END
+
+                    ),
+                    CASE 
+                        WHEN ('bmp' = ANY ($4)) THEN
+                            XMLELEMENT( name monograph_parts,
+                                (SELECT XMLAGG(bmp) FROM (
+                                    SELECT  unapi.bmp( part, 'xml', 'monograph_part', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE)
+                                      FROM  asset.copy_part_map
+                                      WHERE target_copy = cp.id
+                                )x)
+                            )
+                        ELSE NULL
+                    END
                 )
           FROM  serial.unit cp
           WHERE id = $1
index eeb7284..66532cb 100644 (file)
@@ -1149,50 +1149,57 @@ $$;
 
 CREATE OR REPLACE FUNCTION asset.cache_copy_visibility () RETURNS TRIGGER as $func$
 DECLARE
-    add_query       TEXT;
+    add_front       TEXT;
+    add_back        TEXT;
+    add_base_query  TEXT;
+    add_peer_query  TEXT;
     remove_query    TEXT;
     do_add          BOOLEAN := false;
     do_remove       BOOLEAN := false;
 BEGIN
-    add_query := $$
-            INSERT INTO asset.opac_visible_copies (copy_id, circ_lib, record)
-              SELECT id, circ_lib, record FROM (
-                SELECT  cp.id, cp.circ_lib, cn.record, cn.id AS call_number
-                  FROM  asset.copy cp
-                        JOIN asset.call_number cn ON (cn.id = cp.call_number)
-                        JOIN actor.org_unit a ON (cp.circ_lib = a.id)
-                        JOIN asset.copy_location cl ON (cp.location = cl.id)
-                        JOIN config.copy_status cs ON (cp.status = cs.id)
-                        JOIN biblio.record_entry b ON (cn.record = b.id)
-                  WHERE NOT cp.deleted
-                        AND NOT cn.deleted
-                        AND NOT b.deleted
-                        AND cs.opac_visible
-                        AND cl.opac_visible
-                        AND cp.opac_visible
-                        AND a.opac_visible
-                            UNION
-                SELECT  cp.id, cp.circ_lib, pbcm.peer_record AS record, NULL AS call_number
-                  FROM  asset.copy cp
-                        JOIN biblio.peer_bib_copy_map pbcm ON (pbcm.target_copy = cp.id)
-                        JOIN actor.org_unit a ON (cp.circ_lib = a.id)
-                        JOIN asset.copy_location cl ON (cp.location = cl.id)
-                        JOIN config.copy_status cs ON (cp.status = cs.id)
-                  WHERE NOT cp.deleted
-                        AND cs.opac_visible
-                        AND cl.opac_visible
-                        AND cp.opac_visible
-                        AND a.opac_visible
-                    ) AS x 
-
+    add_base_query := $$
+        SELECT  cp.id, cp.circ_lib, cn.record, cn.id AS call_number, cp.location, cp.status
+          FROM  asset.copy cp
+                JOIN asset.call_number cn ON (cn.id = cp.call_number)
+                JOIN actor.org_unit a ON (cp.circ_lib = a.id)
+                JOIN asset.copy_location cl ON (cp.location = cl.id)
+                JOIN config.copy_status cs ON (cp.status = cs.id)
+                JOIN biblio.record_entry b ON (cn.record = b.id)
+          WHERE NOT cp.deleted
+                AND NOT cn.deleted
+                AND NOT b.deleted
+                AND cs.opac_visible
+                AND cl.opac_visible
+                AND cp.opac_visible
+                AND a.opac_visible
+    $$;
+    add_peer_query := $$
+        SELECT  cp.id, cp.circ_lib, pbcm.peer_record AS record, NULL AS call_number, cp.location, cp.status
+          FROM  asset.copy cp
+                JOIN biblio.peer_bib_copy_map pbcm ON (pbcm.target_copy = cp.id)
+                JOIN actor.org_unit a ON (cp.circ_lib = a.id)
+                JOIN asset.copy_location cl ON (cp.location = cl.id)
+                JOIN config.copy_status cs ON (cp.status = cs.id)
+          WHERE NOT cp.deleted
+                AND cs.opac_visible
+                AND cl.opac_visible
+                AND cp.opac_visible
+                AND a.opac_visible
+    $$;
+    add_front := $$
+        INSERT INTO asset.opac_visible_copies (copy_id, circ_lib, record)
+          SELECT id, circ_lib, record FROM (
+    $$;
+    add_back := $$
+        ) AS x
     $$;
  
     remove_query := $$ DELETE FROM asset.opac_visible_copies WHERE copy_id IN ( SELECT id FROM asset.copy WHERE $$;
 
     IF TG_TABLE_NAME = 'peer_bib_copy_map' THEN
         IF TG_OP = 'INSERT' THEN
-            add_query := add_query || 'WHERE x.id = ' || NEW.target_copy || ' AND x.record = ' || NEW.peer_record || ';';
-            EXECUTE add_query;
+            add_peer_query := add_peer_query || ' AND cp.id = ' || NEW.target_copy || ' AND pbcm.record = ' || NEW.peer_record;
+            EXECUTE add_front || add_peer_query || add_back;
             RETURN NEW;
         ELSE
             remove_query := 'DELETE FROM asset.opac_visible_copies WHERE copy_id = ' || OLD.target_copy || ' AND record = ' || OLD.peer_record || ';';
@@ -1204,8 +1211,8 @@ BEGIN
     IF TG_OP = 'INSERT' THEN
 
         IF TG_TABLE_NAME IN ('copy', 'unit') THEN
-            add_query := add_query || 'WHERE x.id = ' || NEW.id || ';';
-            EXECUTE add_query;
+            add_base_query := add_base_query || ' AND cp.id = ' || NEW.id;
+            EXECUTE add_front || add_base_query || add_back;
         END IF;
 
         RETURN NEW;
@@ -1250,8 +1257,9 @@ BEGIN
             DELETE FROM asset.opac_visible_copies WHERE copy_id = NEW.id;
         END IF;
         IF do_add THEN
-            add_query := add_query || 'WHERE x.id = ' || NEW.id || ';';
-            EXECUTE add_query;
+            add_base_query := add_base_query || ' AND cp.id = ' || NEW.id;
+            add_peer_query := add_peer_query || ' AND cp.id = ' || NEW.id;
+            EXECUTE add_front || add_base_query || ' UNION ' || add_peer_query || add_back;
         END IF;
 
         RETURN NEW;
@@ -1276,15 +1284,15 @@ BEGIN
  
         ELSIF OLD.deleted THEN -- add rows
  
-            IF TG_TABLE_NAME IN ('copy','unit') THEN
-                add_query := add_query || 'WHERE x.id = ' || NEW.id || ';';
-            ELSIF TG_TABLE_NAME = 'call_number' THEN
-                add_query := add_query || 'WHERE x.call_number = ' || NEW.id || ';';
+            IF TG_TABLE_NAME = 'call_number' THEN
+                add_base_query := add_base_query || ' AND cn.id = ' || NEW.id;
+                EXECUTE add_front || add_base_query || add_back;
             ELSIF TG_TABLE_NAME = 'record_entry' THEN
-                add_query := add_query || 'WHERE x.record = ' || NEW.id || ';';
+                add_base_query := add_base_query || ' AND cn.record = ' || NEW.id;
+                add_peer_query := add_peer_query || ' AND pbcm.record = ' || NEW.id;
+                EXECUTE add_front || add_base_query || ' UNION ' || add_peer_query || add_back;
             END IF;
  
-            EXECUTE add_query;
             RETURN NEW;
  
         END IF;
@@ -1297,8 +1305,8 @@ BEGIN
             -- call number is linked to different bib
             remove_query := remove_query || 'call_number = ' || NEW.id || ');';
             EXECUTE remove_query;
-            add_query := add_query || 'WHERE x.call_number = ' || NEW.id || ';';
-            EXECUTE add_query;
+            add_base_query := add_base_query || ' AND cn.id = ' || NEW.id;
+            EXECUTE add_front || add_base_query || add_back;
         END IF;
 
         RETURN NEW;
@@ -1317,14 +1325,17 @@ BEGIN
     ELSIF NEW.opac_visible THEN -- add rows
 
         IF TG_TABLE_NAME = 'org_unit' THEN
-            add_query := add_query || 'AND cp.circ_lib = ' || NEW.id || ';';
+            add_base_query := add_base_query || ' AND cp.circ_lib = ' || NEW.id || ';';
+            add_peer_query := add_peer_query || ' AND cp.circ_lib = ' || NEW.id || ';';
         ELSIF TG_TABLE_NAME = 'copy_location' THEN
-            add_query := add_query || 'AND cp.location = ' || NEW.id || ';';
+            add_base_query := add_base_query || ' AND cp.location = ' || NEW.id || ';';
+            add_peer_query := add_peer_query || ' AND cp.location = ' || NEW.id || ';';
         ELSIF TG_TABLE_NAME = 'copy_status' THEN
-            add_query := add_query || 'AND cp.status = ' || NEW.id || ';';
+            add_base_query := add_base_query || ' AND cp.status = ' || NEW.id || ';';
+            add_peer_query := add_peer_query || ' AND cp.status = ' || NEW.id || ';';
         END IF;
  
-        EXECUTE add_query;
+        EXECUTE add_front || add_base_query || ' UNION ' || add_peer_query || add_back;
  
     ELSE -- delete rows
 
index 620c2a4..e320c04 100644 (file)
@@ -1,6 +1,6 @@
 BEGIN;
 
-INSERT INTO config.upgrade_log (version) VALUES ('XXX');
+INSERT INTO config.upgrade_log (version) VALUES ('0544');
 
 INSERT INTO config.usr_setting_type 
 ( name, opac_visible, label, description, datatype) VALUES 
index bdb49bb..428d7a0 100644 (file)
@@ -133,7 +133,9 @@ BEGIN
             FROM asset.call_number acn
                 INNER JOIN asset.uri_call_number_map auricnm ON auricnm.call_number = acn.id
                 INNER JOIN asset.uri auri ON auri.id = auricnm.uri
+                INNER JOIN biblio.record_entry bre ON acn.record = bre.id
             WHERE auri.href = auri.label
+                AND xml_is_well_formed(bre.marc)
             GROUP BY acn.record
             ORDER BY acn.record
         ) AS rec_uris
@@ -149,5 +151,7 @@ $func$ LANGUAGE PLPGSQL;
 -- Kick off the reingest; this may take a while
 SELECT biblio.reingest_uris();
 
+-- Hopefully this isn't something we'll need to run again
+DROP FUNCTION biblio.reingest_uris();
 
 COMMIT;
diff --git a/Open-ILS/src/sql/Pg/upgrade/0560.fix_opac_copy_vis_cache.sql b/Open-ILS/src/sql/Pg/upgrade/0560.fix_opac_copy_vis_cache.sql
new file mode 100644 (file)
index 0000000..73838b9
--- /dev/null
@@ -0,0 +1,210 @@
+-- Evergreen DB patch XXXX.fix_opac_copy_vis_cache.sql
+--
+-- Correct LP#788763: glitch in asset.cache_copy_visibility
+-- prevented updating the visibility of copy locations, org
+-- units, and copy statuses.
+--
+
+BEGIN;
+
+-- check whether patch can be applied
+SELECT evergreen.upgrade_deps_block_check('0560', :eg_version);
+
+CREATE OR REPLACE FUNCTION asset.cache_copy_visibility () RETURNS TRIGGER as $func$
+DECLARE
+    add_query       TEXT;
+    remove_query    TEXT;
+    do_add          BOOLEAN := false;
+    do_remove       BOOLEAN := false;
+BEGIN
+    add_query := $$
+            INSERT INTO asset.opac_visible_copies (copy_id, circ_lib, record)
+              SELECT id, circ_lib, record FROM (
+                SELECT  cp.id, cp.circ_lib, cn.record, cn.id AS call_number, cp.location, cp.status
+                  FROM  asset.copy cp
+                        JOIN asset.call_number cn ON (cn.id = cp.call_number)
+                        JOIN actor.org_unit a ON (cp.circ_lib = a.id)
+                        JOIN asset.copy_location cl ON (cp.location = cl.id)
+                        JOIN config.copy_status cs ON (cp.status = cs.id)
+                        JOIN biblio.record_entry b ON (cn.record = b.id)
+                  WHERE NOT cp.deleted
+                        AND NOT cn.deleted
+                        AND NOT b.deleted
+                        AND cs.opac_visible
+                        AND cl.opac_visible
+                        AND cp.opac_visible
+                        AND a.opac_visible
+                            UNION
+                SELECT  cp.id, cp.circ_lib, pbcm.peer_record AS record, NULL AS call_number, cp.location, cp.status
+                  FROM  asset.copy cp
+                        JOIN biblio.peer_bib_copy_map pbcm ON (pbcm.target_copy = cp.id)
+                        JOIN actor.org_unit a ON (cp.circ_lib = a.id)
+                        JOIN asset.copy_location cl ON (cp.location = cl.id)
+                        JOIN config.copy_status cs ON (cp.status = cs.id)
+                  WHERE NOT cp.deleted
+                        AND cs.opac_visible
+                        AND cl.opac_visible
+                        AND cp.opac_visible
+                        AND a.opac_visible
+                    ) AS x 
+
+    $$;
+    remove_query := $$ DELETE FROM asset.opac_visible_copies WHERE copy_id IN ( SELECT id FROM asset.copy WHERE $$;
+
+    IF TG_TABLE_NAME = 'peer_bib_copy_map' THEN
+        IF TG_OP = 'INSERT' THEN
+            add_query := add_query || 'WHERE x.id = ' || NEW.target_copy || ' AND x.record = ' || NEW.peer_record || ';';
+            EXECUTE add_query;
+            RETURN NEW;
+        ELSE
+            remove_query := 'DELETE FROM asset.opac_visible_copies WHERE copy_id = ' || OLD.target_copy || ' AND record = ' || OLD.peer_record || ';';
+            EXECUTE remove_query;
+            RETURN OLD;
+        END IF;
+    END IF;
+
+    IF TG_OP = 'INSERT' THEN
+
+        IF TG_TABLE_NAME IN ('copy', 'unit') THEN
+            add_query := add_query || 'WHERE x.id = ' || NEW.id || ';';
+            EXECUTE add_query;
+        END IF;
+
+        RETURN NEW;
+
+    END IF;
+
+    -- handle items first, since with circulation activity
+    -- their statuses change frequently
+    IF TG_TABLE_NAME IN ('copy', 'unit') THEN
+
+        IF OLD.location    <> NEW.location OR
+           OLD.call_number <> NEW.call_number OR
+           OLD.status      <> NEW.status OR
+           OLD.circ_lib    <> NEW.circ_lib THEN
+            -- any of these could change visibility, but
+            -- we'll save some queries and not try to calculate
+            -- the change directly
+            do_remove := true;
+            do_add := true;
+        ELSE
+
+            IF OLD.deleted <> NEW.deleted THEN
+                IF NEW.deleted THEN
+                    do_remove := true;
+                ELSE
+                    do_add := true;
+                END IF;
+            END IF;
+
+            IF OLD.opac_visible <> NEW.opac_visible THEN
+                IF OLD.opac_visible THEN
+                    do_remove := true;
+                ELSIF NOT do_remove THEN -- handle edge case where deleted item
+                                        -- is also marked opac_visible
+                    do_add := true;
+                END IF;
+            END IF;
+
+        END IF;
+
+        IF do_remove THEN
+            DELETE FROM asset.opac_visible_copies WHERE copy_id = NEW.id;
+        END IF;
+        IF do_add THEN
+            add_query := add_query || 'WHERE x.id = ' || NEW.id || ';';
+            EXECUTE add_query;
+        END IF;
+
+        RETURN NEW;
+
+    END IF;
+
+    IF TG_TABLE_NAME IN ('call_number', 'record_entry') THEN -- these have a 'deleted' column
+        IF OLD.deleted AND NEW.deleted THEN -- do nothing
+
+            RETURN NEW;
+        ELSIF NEW.deleted THEN -- remove rows
+            IF TG_TABLE_NAME = 'call_number' THEN
+                DELETE FROM asset.opac_visible_copies WHERE copy_id IN (SELECT id FROM asset.copy WHERE call_number = NEW.id);
+            ELSIF TG_TABLE_NAME = 'record_entry' THEN
+                DELETE FROM asset.opac_visible_copies WHERE record = NEW.id;
+            END IF;
+            RETURN NEW;
+        ELSIF OLD.deleted THEN -- add rows
+            IF TG_TABLE_NAME IN ('copy','unit') THEN
+                add_query := add_query || 'WHERE x.id = ' || NEW.id || ';';
+            ELSIF TG_TABLE_NAME = 'call_number' THEN
+                add_query := add_query || 'WHERE x.call_number = ' || NEW.id || ';';
+            ELSIF TG_TABLE_NAME = 'record_entry' THEN
+                add_query := add_query || 'WHERE x.record = ' || NEW.id || ';';
+            END IF;
+            EXECUTE add_query;
+            RETURN NEW;
+        END IF;
+    END IF;
+
+    IF TG_TABLE_NAME = 'call_number' THEN
+
+        IF OLD.record <> NEW.record THEN
+            -- call number is linked to different bib
+            remove_query := remove_query || 'call_number = ' || NEW.id || ');';
+            EXECUTE remove_query;
+            add_query := add_query || 'WHERE x.call_number = ' || NEW.id || ';';
+            EXECUTE add_query;
+        END IF;
+
+        RETURN NEW;
+
+    END IF;
+
+    IF TG_TABLE_NAME IN ('record_entry') THEN
+        RETURN NEW; -- don't have 'opac_visible'
+    END IF;
+
+    -- actor.org_unit, asset.copy_location, asset.copy_status
+    IF NEW.opac_visible = OLD.opac_visible THEN -- do nothing
+
+        RETURN NEW;
+
+    ELSIF NEW.opac_visible THEN -- add rows
+
+        IF TG_TABLE_NAME = 'org_unit' THEN
+            add_query := add_query || 'WHERE x.circ_lib = ' || NEW.id || ';';
+        ELSIF TG_TABLE_NAME = 'copy_location' THEN
+            add_query := add_query || 'WHERE x.location = ' || NEW.id || ';';
+        ELSIF TG_TABLE_NAME = 'copy_status' THEN
+            add_query := add_query || 'WHERE x.status = ' || NEW.id || ';';
+        END IF;
+        EXECUTE add_query;
+    ELSE -- delete rows
+
+        IF TG_TABLE_NAME = 'org_unit' THEN
+            remove_query := 'DELETE FROM asset.opac_visible_copies WHERE circ_lib = ' || NEW.id || ';';
+        ELSIF TG_TABLE_NAME = 'copy_location' THEN
+            remove_query := remove_query || 'location = ' || NEW.id || ');';
+        ELSIF TG_TABLE_NAME = 'copy_status' THEN
+            remove_query := remove_query || 'status = ' || NEW.id || ');';
+        END IF;
+        EXECUTE remove_query;
+    END IF;
+    RETURN NEW;
+END;
+$func$ LANGUAGE PLPGSQL;
+
+COMMIT;
diff --git a/Open-ILS/src/sql/Pg/upgrade/0561.schema.tnf_index.sql b/Open-ILS/src/sql/Pg/upgrade/0561.schema.tnf_index.sql
new file mode 100644 (file)
index 0000000..25383bc
--- /dev/null
@@ -0,0 +1 @@
+-- Just reserve this number to avoid confusion
diff --git a/Open-ILS/src/sql/Pg/upgrade/0562.schema.copy_active_date.sql b/Open-ILS/src/sql/Pg/upgrade/0562.schema.copy_active_date.sql
new file mode 100644 (file)
index 0000000..c2cffff
--- /dev/null
@@ -0,0 +1,628 @@
+-- Evergreen DB patch 0562.schema.copy_active_date.sql
+--
+-- Active Date
+
+BEGIN;
+
+-- check whether patch can be applied
+SELECT evergreen.upgrade_deps_block_check('0562', :eg_version);
+
+ALTER TABLE asset.copy
+    ADD COLUMN active_date TIMESTAMP WITH TIME ZONE;
+
+ALTER TABLE auditor.asset_copy_history
+    ADD COLUMN active_date TIMESTAMP WITH TIME ZONE;
+
+ALTER TABLE auditor.serial_unit_history
+    ADD COLUMN active_date TIMESTAMP WITH TIME ZONE;
+
+ALTER TABLE config.copy_status
+    ADD COLUMN copy_active BOOL NOT NULL DEFAULT FALSE;
+
+ALTER TABLE config.circ_matrix_weights
+    ADD COLUMN item_age NUMERIC(6,2) NOT NULL DEFAULT 0.0;
+
+ALTER TABLE config.hold_matrix_weights
+    ADD COLUMN item_age NUMERIC(6,2) NOT NULL DEFAULT 0.0;
+
+-- The two defaults above were to stop erroring on NOT NULL
+-- Remove them here
+ALTER TABLE config.circ_matrix_weights
+    ALTER COLUMN item_age DROP DEFAULT;
+
+ALTER TABLE config.hold_matrix_weights
+    ALTER COLUMN item_age DROP DEFAULT;
+
+ALTER TABLE config.circ_matrix_matchpoint
+    ADD COLUMN item_age INTERVAL;
+
+ALTER TABLE config.hold_matrix_matchpoint
+    ADD COLUMN item_age INTERVAL;
+
+CREATE OR REPLACE FUNCTION asset.acp_status_changed()
+RETURNS TRIGGER AS $$
+BEGIN
+    IF NEW.status <> OLD.status THEN
+        NEW.status_changed_time := now();
+        IF NEW.active_date IS NULL AND NEW.status IN (SELECT id FROM config.copy_status WHERE copy_active = true) THEN
+            NEW.active_date := now();
+        END IF;
+    END IF;
+    RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION asset.acp_created()
+RETURNS TRIGGER AS $$
+BEGIN
+    IF NEW.active_date IS NULL AND NEW.status IN (SELECT id FROM config.copy_status WHERE copy_active = true) THEN
+        NEW.active_date := now();
+    END IF;
+    IF NEW.status_changed_time IS NULL THEN
+        NEW.status_changed_time := now();
+    END IF;
+    RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE TRIGGER acp_created_trig
+    BEFORE INSERT ON asset.copy
+    FOR EACH ROW EXECUTE PROCEDURE asset.acp_created();
+
+CREATE TRIGGER sunit_created_trig
+    BEFORE INSERT ON serial.unit
+    FOR EACH ROW EXECUTE PROCEDURE asset.acp_created();
+
+CREATE OR REPLACE FUNCTION action.hold_request_permit_test( pickup_ou INT, request_ou INT, match_item BIGINT, match_user INT, match_requestor INT, retargetting BOOL ) RETURNS SETOF action.matrix_test_result AS $func$
+DECLARE
+    matchpoint_id        INT;
+    user_object        actor.usr%ROWTYPE;
+    age_protect_object    config.rule_age_hold_protect%ROWTYPE;
+    standing_penalty    config.standing_penalty%ROWTYPE;
+    transit_range_ou_type    actor.org_unit_type%ROWTYPE;
+    transit_source        actor.org_unit%ROWTYPE;
+    item_object        asset.copy%ROWTYPE;
+    item_cn_object     asset.call_number%ROWTYPE;
+    ou_skip              actor.org_unit_setting%ROWTYPE;
+    result            action.matrix_test_result;
+    hold_test        config.hold_matrix_matchpoint%ROWTYPE;
+    use_active_date   TEXT;
+    age_protect_date  TIMESTAMP WITH TIME ZONE;
+    hold_count        INT;
+    hold_transit_prox    INT;
+    frozen_hold_count    INT;
+    context_org_list    INT[];
+    done            BOOL := FALSE;
+BEGIN
+    SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
+    SELECT INTO context_org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( pickup_ou );
+
+    result.success := TRUE;
+
+    -- Fail if we couldn't find a user
+    IF user_object.id IS NULL THEN
+        result.fail_part := 'no_user';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+        RETURN;
+    END IF;
+
+    SELECT INTO item_object * FROM asset.copy WHERE id = match_item;
+
+    -- Fail if we couldn't find a copy
+    IF item_object.id IS NULL THEN
+        result.fail_part := 'no_item';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+        RETURN;
+    END IF;
+
+    SELECT INTO matchpoint_id action.find_hold_matrix_matchpoint(pickup_ou, request_ou, match_item, match_user, match_requestor);
+    result.matchpoint := matchpoint_id;
+
+    SELECT INTO ou_skip * FROM actor.org_unit_setting WHERE name = 'circ.holds.target_skip_me' AND org_unit = item_object.circ_lib;
+
+    -- Fail if the circ_lib for the item has circ.holds.target_skip_me set to true
+    IF ou_skip.id IS NOT NULL AND ou_skip.value = 'true' THEN
+        result.fail_part := 'circ.holds.target_skip_me';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+        RETURN;
+    END IF;
+
+    -- Fail if user is barred
+    IF user_object.barred IS TRUE THEN
+        result.fail_part := 'actor.usr.barred';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+        RETURN;
+    END IF;
+
+    -- Fail if we couldn't find any matchpoint (requires a default)
+    IF matchpoint_id IS NULL THEN
+        result.fail_part := 'no_matchpoint';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+        RETURN;
+    END IF;
+
+    SELECT INTO hold_test * FROM config.hold_matrix_matchpoint WHERE id = matchpoint_id;
+
+    IF hold_test.holdable IS FALSE THEN
+        result.fail_part := 'config.hold_matrix_test.holdable';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    END IF;
+
+    IF hold_test.transit_range IS NOT NULL THEN
+        SELECT INTO transit_range_ou_type * FROM actor.org_unit_type WHERE id = hold_test.transit_range;
+        IF hold_test.distance_is_from_owner THEN
+            SELECT INTO transit_source ou.* FROM actor.org_unit ou JOIN asset.call_number cn ON (cn.owning_lib = ou.id) WHERE cn.id = item_object.call_number;
+        ELSE
+            SELECT INTO transit_source * FROM actor.org_unit WHERE id = item_object.circ_lib;
+        END IF;
+
+        PERFORM * FROM actor.org_unit_descendants( transit_source.id, transit_range_ou_type.depth ) WHERE id = pickup_ou;
+
+        IF NOT FOUND THEN
+            result.fail_part := 'transit_range';
+            result.success := FALSE;
+            done := TRUE;
+            RETURN NEXT result;
+        END IF;
+    END IF;
+    FOR standing_penalty IN
+        SELECT  DISTINCT csp.*
+          FROM  actor.usr_standing_penalty usp
+                JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
+          WHERE usr = match_user
+                AND usp.org_unit IN ( SELECT * FROM explode_array(context_org_list) )
+                AND (usp.stop_date IS NULL or usp.stop_date > NOW())
+                AND csp.block_list LIKE '%HOLD%' LOOP
+
+        result.fail_part := standing_penalty.name;
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    END LOOP;
+
+    IF hold_test.stop_blocked_user IS TRUE THEN
+        FOR standing_penalty IN
+            SELECT  DISTINCT csp.*
+              FROM  actor.usr_standing_penalty usp
+                    JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
+              WHERE usr = match_user
+                    AND usp.org_unit IN ( SELECT * FROM explode_array(context_org_list) )
+                    AND (usp.stop_date IS NULL or usp.stop_date > NOW())
+                    AND csp.block_list LIKE '%CIRC%' LOOP
+    
+            result.fail_part := standing_penalty.name;
+            result.success := FALSE;
+            done := TRUE;
+            RETURN NEXT result;
+        END LOOP;
+    END IF;
+
+    IF hold_test.max_holds IS NOT NULL AND NOT retargetting THEN
+        SELECT    INTO hold_count COUNT(*)
+          FROM    action.hold_request
+          WHERE    usr = match_user
+            AND fulfillment_time IS NULL
+            AND cancel_time IS NULL
+            AND CASE WHEN hold_test.include_frozen_holds THEN TRUE ELSE frozen IS FALSE END;
+
+        IF hold_count >= hold_test.max_holds THEN
+            result.fail_part := 'config.hold_matrix_test.max_holds';
+            result.success := FALSE;
+            done := TRUE;
+            RETURN NEXT result;
+        END IF;
+    END IF;
+
+    IF item_object.age_protect IS NOT NULL THEN
+        SELECT INTO age_protect_object * FROM config.rule_age_hold_protect WHERE id = item_object.age_protect;
+        IF hold_test.distance_is_from_owner THEN
+            SELECT INTO use_active_date value FROM actor.org_unit_ancestor_setting('circ.holds.age_protect.active_date', item_cn_object.owning_lib);
+        ELSE
+            SELECT INTO use_active_date value FROM actor.org_unit_ancestor_setting('circ.holds.age_protect.active_date', item_object.circ_lib);
+        END IF;
+        IF use_active_date = 'true' THEN
+            age_protect_date := COALESCE(item_object.active_date, NOW());
+        ELSE
+            age_protect_date := item_object.create_date;
+        END IF;
+        IF age_protect_date + age_protect_object.age > NOW() THEN
+            IF hold_test.distance_is_from_owner THEN
+                SELECT INTO item_cn_object * FROM asset.call_number WHERE id = item_object.call_number;
+                SELECT INTO hold_transit_prox prox FROM actor.org_unit_proximity WHERE from_org = item_cn_object.owning_lib AND to_org = pickup_ou;
+            ELSE
+                SELECT INTO hold_transit_prox prox FROM actor.org_unit_proximity WHERE from_org = item_object.circ_lib AND to_org = pickup_ou;
+            END IF;
+
+            IF hold_transit_prox > age_protect_object.prox THEN
+                result.fail_part := 'config.rule_age_hold_protect.prox';
+                result.success := FALSE;
+                done := TRUE;
+                RETURN NEXT result;
+            END IF;
+        END IF;
+    END IF;
+
+    IF NOT done THEN
+        RETURN NEXT result;
+    END IF;
+
+    RETURN;
+END;
+$func$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION action.find_circ_matrix_matchpoint( context_ou INT, item_object asset.copy, user_object actor.usr, renewal BOOL ) RETURNS action.found_circ_matrix_matchpoint AS $func$
+DECLARE
+    cn_object       asset.call_number%ROWTYPE;
+    rec_descriptor  metabib.rec_descriptor%ROWTYPE;
+    cur_matchpoint  config.circ_matrix_matchpoint%ROWTYPE;
+    matchpoint      config.circ_matrix_matchpoint%ROWTYPE;
+    weights         config.circ_matrix_weights%ROWTYPE;
+    user_age        INTERVAL;
+    my_item_age     INTERVAL;
+    denominator     NUMERIC(6,2);
+    row_list        INT[];
+    result          action.found_circ_matrix_matchpoint;
+BEGIN
+    -- Assume failure
+    result.success = false;
+
+    -- Fetch useful data
+    SELECT INTO cn_object       * FROM asset.call_number        WHERE id = item_object.call_number;
+    SELECT INTO rec_descriptor  * FROM metabib.rec_descriptor   WHERE record = cn_object.record;
+
+    -- Pre-generate this so we only calc it once
+    IF user_object.dob IS NOT NULL THEN
+        SELECT INTO user_age age(user_object.dob);
+    END IF;
+
+    -- Ditto
+    SELECT INTO my_item_age age(coalesce(item_object.active_date, now()));
+
+    -- Grab the closest set circ weight setting.
+    SELECT INTO weights cw.*
+      FROM config.weight_assoc wa
+           JOIN config.circ_matrix_weights cw ON (cw.id = wa.circ_weights)
+           JOIN actor.org_unit_ancestors_distance( context_ou ) d ON (wa.org_unit = d.id)
+      WHERE active
+      ORDER BY d.distance
+      LIMIT 1;
+
+    -- No weights? Bad admin! Defaults to handle that anyway.
+    IF weights.id IS NULL THEN
+        weights.grp                 := 11.0;
+        weights.org_unit            := 10.0;
+        weights.circ_modifier       := 5.0;
+        weights.marc_type           := 4.0;
+        weights.marc_form           := 3.0;
+        weights.marc_bib_level      := 2.0;
+        weights.marc_vr_format      := 2.0;
+        weights.copy_circ_lib       := 8.0;
+        weights.copy_owning_lib     := 8.0;
+        weights.user_home_ou        := 8.0;
+        weights.ref_flag            := 1.0;
+        weights.juvenile_flag       := 6.0;
+        weights.is_renewal          := 7.0;
+        weights.usr_age_lower_bound := 0.0;
+        weights.usr_age_upper_bound := 0.0;
+        weights.item_age            := 0.0;
+    END IF;
+
+    -- Determine the max (expected) depth (+1) of the org tree and max depth of the permisson tree
+    -- If you break your org tree with funky parenting this may be wrong
+    -- Note: This CTE is duplicated in the find_hold_matrix_matchpoint function, and it may be a good idea to split it off to a function
+    -- We use one denominator for all tree-based checks for when permission groups and org units have the same weighting
+    WITH all_distance(distance) AS (
+            SELECT depth AS distance FROM actor.org_unit_type
+        UNION
+                   SELECT distance AS distance FROM permission.grp_ancestors_distance((SELECT id FROM permission.grp_tree WHERE parent IS NULL))
+       )
+    SELECT INTO denominator MAX(distance) + 1 FROM all_distance;
+
+    -- Loop over all the potential matchpoints
+    FOR cur_matchpoint IN
+        SELECT m.*
+          FROM  config.circ_matrix_matchpoint m
+                /*LEFT*/ JOIN permission.grp_ancestors_distance( user_object.profile ) upgad ON m.grp = upgad.id
+                /*LEFT*/ JOIN actor.org_unit_ancestors_distance( context_ou ) ctoua ON m.org_unit = ctoua.id
+                LEFT JOIN actor.org_unit_ancestors_distance( cn_object.owning_lib ) cnoua ON m.copy_owning_lib = cnoua.id
+                LEFT JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) iooua ON m.copy_circ_lib = iooua.id
+                LEFT JOIN actor.org_unit_ancestors_distance( user_object.home_ou  ) uhoua ON m.user_home_ou = uhoua.id
+          WHERE m.active
+                -- Permission Groups
+             -- AND (m.grp                      IS NULL OR upgad.id IS NOT NULL) -- Optional Permission Group?
+                -- Org Units
+             -- AND (m.org_unit                 IS NULL OR ctoua.id IS NOT NULL) -- Optional Org Unit?
+                AND (m.copy_owning_lib          IS NULL OR cnoua.id IS NOT NULL)
+                AND (m.copy_circ_lib            IS NULL OR iooua.id IS NOT NULL)
+                AND (m.user_home_ou             IS NULL OR uhoua.id IS NOT NULL)
+                -- Circ Type
+                AND (m.is_renewal               IS NULL OR m.is_renewal = renewal)
+                -- Static User Checks
+                AND (m.juvenile_flag            IS NULL OR m.juvenile_flag = user_object.juvenile)
+                AND (m.usr_age_lower_bound      IS NULL OR (user_age IS NOT NULL AND m.usr_age_lower_bound < user_age))
+                AND (m.usr_age_upper_bound      IS NULL OR (user_age IS NOT NULL AND m.usr_age_upper_bound > user_age))
+                -- Static Item Checks
+                AND (m.circ_modifier            IS NULL OR m.circ_modifier = item_object.circ_modifier)
+                AND (m.marc_type                IS NULL OR m.marc_type = COALESCE(item_object.circ_as_type, rec_descriptor.item_type))
+                AND (m.marc_form                IS NULL OR m.marc_form = rec_descriptor.item_form)
+                AND (m.marc_bib_level           IS NULL OR m.marc_bib_level = rec_descriptor.bib_level)
+                AND (m.marc_vr_format           IS NULL OR m.marc_vr_format = rec_descriptor.vr_format)
+                AND (m.ref_flag                 IS NULL OR m.ref_flag = item_object.ref)
+                AND (m.item_age                 IS NULL OR (my_item_age IS NOT NULL AND m.item_age > my_item_age))
+          ORDER BY
+                -- Permission Groups
+                CASE WHEN upgad.distance        IS NOT NULL THEN 2^(2*weights.grp - (upgad.distance/denominator)) ELSE 0.0 END +
+                -- Org Units
+                CASE WHEN ctoua.distance        IS NOT NULL THEN 2^(2*weights.org_unit - (ctoua.distance/denominator)) ELSE 0.0 END +
+                CASE WHEN cnoua.distance        IS NOT NULL THEN 2^(2*weights.copy_owning_lib - (cnoua.distance/denominator)) ELSE 0.0 END +
+                CASE WHEN iooua.distance        IS NOT NULL THEN 2^(2*weights.copy_circ_lib - (iooua.distance/denominator)) ELSE 0.0 END +
+                CASE WHEN uhoua.distance        IS NOT NULL THEN 2^(2*weights.user_home_ou - (uhoua.distance/denominator)) ELSE 0.0 END +
+                -- Circ Type                    -- Note: 4^x is equiv to 2^(2*x)
+                CASE WHEN m.is_renewal          IS NOT NULL THEN 4^weights.is_renewal ELSE 0.0 END +
+                -- Static User Checks
+                CASE WHEN m.juvenile_flag       IS NOT NULL THEN 4^weights.juvenile_flag ELSE 0.0 END +
+                CASE WHEN m.usr_age_lower_bound IS NOT NULL THEN 4^weights.usr_age_lower_bound ELSE 0.0 END +
+                CASE WHEN m.usr_age_upper_bound IS NOT NULL THEN 4^weights.usr_age_upper_bound ELSE 0.0 END +
+                -- Static Item Checks
+                CASE WHEN m.circ_modifier       IS NOT NULL THEN 4^weights.circ_modifier ELSE 0.0 END +
+                CASE WHEN m.marc_type           IS NOT NULL THEN 4^weights.marc_type ELSE 0.0 END +
+                CASE WHEN m.marc_form           IS NOT NULL THEN 4^weights.marc_form ELSE 0.0 END +
+                CASE WHEN m.marc_vr_format      IS NOT NULL THEN 4^weights.marc_vr_format ELSE 0.0 END +
+                CASE WHEN m.ref_flag            IS NOT NULL THEN 4^weights.ref_flag ELSE 0.0 END +
+                -- Item age has a slight adjustment to weight based on value.
+                -- This should ensure that a shorter age limit comes first when all else is equal.
+                -- NOTE: This assumes that intervals will normally be in days.
+                CASE WHEN m.item_age            IS NOT NULL THEN 4^weights.item_age - 1 + 86400/EXTRACT(EPOCH FROM m.item_age) ELSE 0.0 END DESC,
+                -- Final sort on id, so that if two rules have the same sorting in the previous sort they have a defined order
+                -- This prevents "we changed the table order by updating a rule, and we started getting different results"
+                m.id LOOP
+
+        -- Record the full matching row list
+        row_list := row_list || cur_matchpoint.id;
+
+        -- No matchpoint yet?
+        IF matchpoint.id IS NULL THEN
+            -- Take the entire matchpoint as a starting point
+            matchpoint := cur_matchpoint;
+            CONTINUE; -- No need to look at this row any more.
+        END IF;
+
+        -- Incomplete matchpoint?
+        IF matchpoint.circulate IS NULL THEN
+            matchpoint.circulate := cur_matchpoint.circulate;
+        END IF;
+        IF matchpoint.duration_rule IS NULL THEN
+            matchpoint.duration_rule := cur_matchpoint.duration_rule;
+        END IF;
+        IF matchpoint.recurring_fine_rule IS NULL THEN
+            matchpoint.recurring_fine_rule := cur_matchpoint.recurring_fine_rule;
+        END IF;
+        IF matchpoint.max_fine_rule IS NULL THEN
+            matchpoint.max_fine_rule := cur_matchpoint.max_fine_rule;
+        END IF;
+        IF matchpoint.hard_due_date IS NULL THEN
+            matchpoint.hard_due_date := cur_matchpoint.hard_due_date;
+        END IF;
+        IF matchpoint.total_copy_hold_ratio IS NULL THEN
+            matchpoint.total_copy_hold_ratio := cur_matchpoint.total_copy_hold_ratio;
+        END IF;
+        IF matchpoint.available_copy_hold_ratio IS NULL THEN
+            matchpoint.available_copy_hold_ratio := cur_matchpoint.available_copy_hold_ratio;
+        END IF;
+        IF matchpoint.renewals IS NULL THEN
+            matchpoint.renewals := cur_matchpoint.renewals;
+        END IF;
+        IF matchpoint.grace_period IS NULL THEN
+            matchpoint.grace_period := cur_matchpoint.grace_period;
+        END IF;
+    END LOOP;
+
+    -- Check required fields
+    IF matchpoint.circulate             IS NOT NULL AND
+       matchpoint.duration_rule         IS NOT NULL AND
+       matchpoint.recurring_fine_rule   IS NOT NULL AND
+       matchpoint.max_fine_rule         IS NOT NULL THEN
+        -- All there? We have a completed match.
+        result.success := true;
+    END IF;
+
+    -- Include the assembled matchpoint, even if it isn't complete
+    result.matchpoint := matchpoint;
+
+    -- Include (for debugging) the full list of matching rows
+    result.buildrows := row_list;
+
+    -- Hand the result back to caller
+    RETURN result;
+END;
+$func$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION action.find_hold_matrix_matchpoint(pickup_ou integer, request_ou integer, match_item bigint, match_user integer, match_requestor integer)
+  RETURNS integer AS
+$func$
+DECLARE
+    requestor_object    actor.usr%ROWTYPE;
+    user_object         actor.usr%ROWTYPE;
+    item_object         asset.copy%ROWTYPE;
+    item_cn_object      asset.call_number%ROWTYPE;
+    my_item_age         INTERVAL;
+    rec_descriptor      metabib.rec_descriptor%ROWTYPE;
+    matchpoint          config.hold_matrix_matchpoint%ROWTYPE;
+    weights             config.hold_matrix_weights%ROWTYPE;
+    denominator         NUMERIC(6,2);
+BEGIN
+    SELECT INTO user_object         * FROM actor.usr                WHERE id = match_user;
+    SELECT INTO requestor_object    * FROM actor.usr                WHERE id = match_requestor;
+    SELECT INTO item_object         * FROM asset.copy               WHERE id = match_item;
+    SELECT INTO item_cn_object      * FROM asset.call_number        WHERE id = item_object.call_number;
+    SELECT INTO rec_descriptor      * FROM metabib.rec_descriptor   WHERE record = item_cn_object.record;
+
+    SELECT INTO my_item_age age(coalesce(item_object.active_date, now()));
+
+    -- The item's owner should probably be the one determining if the item is holdable
+    -- How to decide that is debatable. Decided to default to the circ library (where the item lives)
+    -- This flag will allow for setting it to the owning library (where the call number "lives")
+    PERFORM * FROM config.internal_flag WHERE name = 'circ.holds.weight_owner_not_circ' AND enabled;
+
+    -- Grab the closest set circ weight setting.
+    IF NOT FOUND THEN
+        -- Default to circ library
+        SELECT INTO weights hw.*
+          FROM config.weight_assoc wa
+               JOIN config.hold_matrix_weights hw ON (hw.id = wa.hold_weights)
+               JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) d ON (wa.org_unit = d.id)
+          WHERE active
+          ORDER BY d.distance
+          LIMIT 1;
+    ELSE
+        -- Flag is set, use owning library
+        SELECT INTO weights hw.*
+          FROM config.weight_assoc wa
+               JOIN config.hold_matrix_weights hw ON (hw.id = wa.hold_weights)
+               JOIN actor.org_unit_ancestors_distance( item_cn_object.owning_lib ) d ON (wa.org_unit = d.id)
+          WHERE active
+          ORDER BY d.distance
+          LIMIT 1;
+    END IF;
+
+    -- No weights? Bad admin! Defaults to handle that anyway.
+    IF weights.id IS NULL THEN
+        weights.user_home_ou    := 5.0;
+        weights.request_ou      := 5.0;
+        weights.pickup_ou       := 5.0;
+        weights.item_owning_ou  := 5.0;
+        weights.item_circ_ou    := 5.0;
+        weights.usr_grp         := 7.0;
+        weights.requestor_grp   := 8.0;
+        weights.circ_modifier   := 4.0;
+        weights.marc_type       := 3.0;
+        weights.marc_form       := 2.0;
+        weights.marc_bib_level  := 1.0;
+        weights.marc_vr_format  := 1.0;
+        weights.juvenile_flag   := 4.0;
+        weights.ref_flag        := 0.0;
+        weights.item_age        := 0.0;
+    END IF;
+
+    -- Determine the max (expected) depth (+1) of the org tree and max depth of the permisson tree
+    -- If you break your org tree with funky parenting this may be wrong
+    -- Note: This CTE is duplicated in the find_circ_matrix_matchpoint function, and it may be a good idea to split it off to a function
+    -- We use one denominator for all tree-based checks for when permission groups and org units have the same weighting
+    WITH all_distance(distance) AS (
+            SELECT depth AS distance FROM actor.org_unit_type
+        UNION
+            SELECT distance AS distance FROM permission.grp_ancestors_distance((SELECT id FROM permission.grp_tree WHERE parent IS NULL))
+       )
+    SELECT INTO denominator MAX(distance) + 1 FROM all_distance;
+
+    -- To ATTEMPT to make this work like it used to, make it reverse the user/requestor profile ids.
+    -- This may be better implemented as part of the upgrade script?
+    -- Set usr_grp = requestor_grp, requestor_grp = 1 or something when this flag is already set
+    -- Then remove this flag, of course.
+    PERFORM * FROM config.internal_flag WHERE name = 'circ.holds.usr_not_requestor' AND enabled;
+
+    IF FOUND THEN
+        -- Note: This, to me, is REALLY hacky. I put it in anyway.
+        -- If you can't tell, this is a single call swap on two variables.
+        SELECT INTO user_object.profile, requestor_object.profile
+                    requestor_object.profile, user_object.profile;
+    END IF;
+
+    -- Select the winning matchpoint into the matchpoint variable for returning
+    SELECT INTO matchpoint m.*
+      FROM  config.hold_matrix_matchpoint m
+            /*LEFT*/ JOIN permission.grp_ancestors_distance( requestor_object.profile ) rpgad ON m.requestor_grp = rpgad.id
+            LEFT JOIN permission.grp_ancestors_distance( user_object.profile ) upgad ON m.usr_grp = upgad.id
+            LEFT JOIN actor.org_unit_ancestors_distance( pickup_ou ) puoua ON m.pickup_ou = puoua.id
+            LEFT JOIN actor.org_unit_ancestors_distance( request_ou ) rqoua ON m.request_ou = rqoua.id
+            LEFT JOIN actor.org_unit_ancestors_distance( item_cn_object.owning_lib ) cnoua ON m.item_owning_ou = cnoua.id
+            LEFT JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) iooua ON m.item_circ_ou = iooua.id
+            LEFT JOIN actor.org_unit_ancestors_distance( user_object.home_ou  ) uhoua ON m.user_home_ou = uhoua.id
+      WHERE m.active
+            -- Permission Groups
+         -- AND (m.requestor_grp        IS NULL OR upgad.id IS NOT NULL) -- Optional Requestor Group?
+            AND (m.usr_grp              IS NULL OR upgad.id IS NOT NULL)
+            -- Org Units
+            AND (m.pickup_ou            IS NULL OR (puoua.id IS NOT NULL AND (puoua.distance = 0 OR NOT m.strict_ou_match)))
+            AND (m.request_ou           IS NULL OR (rqoua.id IS NOT NULL AND (rqoua.distance = 0 OR NOT m.strict_ou_match)))
+            AND (m.item_owning_ou       IS NULL OR (cnoua.id IS NOT NULL AND (cnoua.distance = 0 OR NOT m.strict_ou_match)))
+            AND (m.item_circ_ou         IS NULL OR (iooua.id IS NOT NULL AND (iooua.distance = 0 OR NOT m.strict_ou_match)))
+            AND (m.user_home_ou         IS NULL OR (uhoua.id IS NOT NULL AND (uhoua.distance = 0 OR NOT m.strict_ou_match)))
+            -- Static User Checks
+            AND (m.juvenile_flag        IS NULL OR m.juvenile_flag = user_object.juvenile)
+            -- Static Item Checks
+            AND (m.circ_modifier        IS NULL OR m.circ_modifier = item_object.circ_modifier)
+            AND (m.marc_type            IS NULL OR m.marc_type = COALESCE(item_object.circ_as_type, rec_descriptor.item_type))
+            AND (m.marc_form            IS NULL OR m.marc_form = rec_descriptor.item_form)
+            AND (m.marc_bib_level       IS NULL OR m.marc_bib_level = rec_descriptor.bib_level)
+            AND (m.marc_vr_format       IS NULL OR m.marc_vr_format = rec_descriptor.vr_format)
+            AND (m.ref_flag             IS NULL OR m.ref_flag = item_object.ref)
+            AND (m.item_age             IS NULL OR (my_item_age IS NOT NULL AND m.item_age > my_item_age))
+      ORDER BY
+            -- Permission Groups
+            CASE WHEN rpgad.distance    IS NOT NULL THEN 2^(2*weights.requestor_grp - (rpgad.distance/denominator)) ELSE 0.0 END +
+            CASE WHEN upgad.distance    IS NOT NULL THEN 2^(2*weights.usr_grp - (upgad.distance/denominator)) ELSE 0.0 END +
+            -- Org Units
+            CASE WHEN puoua.distance    IS NOT NULL THEN 2^(2*weights.pickup_ou - (puoua.distance/denominator)) ELSE 0.0 END +
+            CASE WHEN rqoua.distance    IS NOT NULL THEN 2^(2*weights.request_ou - (rqoua.distance/denominator)) ELSE 0.0 END +
+            CASE WHEN cnoua.distance    IS NOT NULL THEN 2^(2*weights.item_owning_ou - (cnoua.distance/denominator)) ELSE 0.0 END +
+            CASE WHEN iooua.distance    IS NOT NULL THEN 2^(2*weights.item_circ_ou - (iooua.distance/denominator)) ELSE 0.0 END +
+            CASE WHEN uhoua.distance    IS NOT NULL THEN 2^(2*weights.user_home_ou - (uhoua.distance/denominator)) ELSE 0.0 END +
+            -- Static User Checks       -- Note: 4^x is equiv to 2^(2*x)
+            CASE WHEN m.juvenile_flag   IS NOT NULL THEN 4^weights.juvenile_flag ELSE 0.0 END +
+            -- Static Item Checks
+            CASE WHEN m.circ_modifier   IS NOT NULL THEN 4^weights.circ_modifier ELSE 0.0 END +
+            CASE WHEN m.marc_type       IS NOT NULL THEN 4^weights.marc_type ELSE 0.0 END +
+            CASE WHEN m.marc_form       IS NOT NULL THEN 4^weights.marc_form ELSE 0.0 END +
+            CASE WHEN m.marc_vr_format  IS NOT NULL THEN 4^weights.marc_vr_format ELSE 0.0 END +
+            CASE WHEN m.ref_flag        IS NOT NULL THEN 4^weights.ref_flag ELSE 0.0 END +
+            -- Item age has a slight adjustment to weight based on value.
+            -- This should ensure that a shorter age limit comes first when all else is equal.
+            -- NOTE: This assumes that intervals will normally be in days.
+            CASE WHEN m.item_age            IS NOT NULL THEN 4^weights.item_age - 86400/EXTRACT(EPOCH FROM m.item_age) ELSE 0.0 END DESC,
+            -- Final sort on id, so that if two rules have the same sorting in the previous sort they have a defined order
+            -- This prevents "we changed the table order by updating a rule, and we started getting different results"
+            m.id;
+
+    -- Return just the ID for now
+    RETURN matchpoint.id;
+END;
+$func$ LANGUAGE 'plpgsql';
+
+DROP INDEX IF EXISTS config.ccmm_once_per_paramset;
+
+DROP INDEX IF EXISTS config.chmm_once_per_paramset;
+
+CREATE UNIQUE INDEX ccmm_once_per_paramset ON config.circ_matrix_matchpoint (org_unit, grp, COALESCE(circ_modifier, ''), COALESCE(marc_type, ''), COALESCE(marc_form, ''), COALESCE(marc_bib_level,''), COALESCE(marc_vr_format, ''), COALESCE(copy_circ_lib::TEXT, ''), COALESCE(copy_owning_lib::TEXT, ''), COALESCE(user_home_ou::TEXT, ''), COALESCE(ref_flag::TEXT, ''), COALESCE(juvenile_flag::TEXT, ''), COALESCE(is_renewal::TEXT, ''), COALESCE(usr_age_lower_bound::TEXT, ''), COALESCE(usr_age_upper_bound::TEXT, ''), COALESCE(item_age::TEXT, '')) WHERE active;
+
+CREATE UNIQUE INDEX chmm_once_per_paramset ON config.hold_matrix_matchpoint (COALESCE(user_home_ou::TEXT, ''), COALESCE(request_ou::TEXT, ''), COALESCE(pickup_ou::TEXT, ''), COALESCE(item_owning_ou::TEXT, ''), COALESCE(item_circ_ou::TEXT, ''), COALESCE(usr_grp::TEXT, ''), COALESCE(requestor_grp::TEXT, ''), COALESCE(circ_modifier, ''), COALESCE(marc_type, ''), COALESCE(marc_form, ''), COALESCE(marc_bib_level, ''), COALESCE(marc_vr_format, ''), COALESCE(juvenile_flag::TEXT, ''), COALESCE(ref_flag::TEXT, ''), COALESCE(item_age::TEXT, '')) WHERE active;
+
+UPDATE config.copy_status SET copy_active = true WHERE id IN (0, 1, 7, 8, 10, 12, 15);
+
+INSERT into config.org_unit_setting_type
+( name, label, description, datatype ) VALUES
+( 'circ.holds.age_protect.active_date', 'Holds: Use Active Date for Age Protection', 'When calculating age protection rules use the active date instead of the creation date.', 'bool');
+
+-- Assume create date when item is in status we would update active date for anyway
+UPDATE asset.copy SET active_date = create_date WHERE status IN (SELECT id FROM config.copy_status WHERE copy_active = true);
+
+-- Assume create date for any item with circs
+UPDATE asset.copy SET active_date = create_date WHERE id IN (SELECT id FROM extend_reporter.full_circ_count WHERE circ_count > 0);
+
+-- Assume create date for status change time while we are at it. Because being created WAS a change in status.
+UPDATE asset.copy SET status_changed_time = create_date WHERE status_changed_time IS NULL;
+
+COMMIT;
diff --git a/Open-ILS/src/sql/Pg/upgrade/0563.data.collections_exempt_perm.sql b/Open-ILS/src/sql/Pg/upgrade/0563.data.collections_exempt_perm.sql
new file mode 100644 (file)
index 0000000..a7e6856
--- /dev/null
@@ -0,0 +1,26 @@
+-- Evergreen DB patch XXXX.data.collections_exempt_perm.sql
+--
+-- Adds a new UPDATE_PATRON_COLLECTIONS_EXEMPT permission
+--
+BEGIN;
+
+
+-- check whether patch can be applied
+SELECT evergreen.upgrade_deps_block_check('0563', :eg_version);
+
+INSERT INTO permission.perm_list ( id, code, description ) 
+    VALUES ( 510, 'UPDATE_PATRON_COLLECTIONS_EXEMPT', oils_i18n_gettext(510,
+    'Allows a user to indicate that a patron is exempt from collections processing', 'ppl', 'description'));
+
+--- stock Circulation Administrator group
+
+INSERT INTO permission.grp_perm_map ( grp, perm, depth, grantable )
+    SELECT
+        4,
+        id,
+        0,
+        't'
+    FROM permission.perm_list
+    WHERE code in ('UPDATE_PATRON_COLLECTIONS_EXEMPT');
+
+COMMIT;
diff --git a/Open-ILS/src/sql/Pg/upgrade/0564.data.org-setting-cat.volume.delete_on_empty.sql b/Open-ILS/src/sql/Pg/upgrade/0564.data.org-setting-cat.volume.delete_on_empty.sql
new file mode 100644 (file)
index 0000000..d5c5213
--- /dev/null
@@ -0,0 +1,19 @@
+-- Evergreen DB patch 0564.data.delete_empty_volume.sql
+--
+-- New org setting cat.volume.delete_on_empty
+--
+BEGIN;
+
+-- check whether patch can be applied
+SELECT evergreen.upgrade_deps_block_check('0564', :eg_version);
+
+INSERT INTO config.org_unit_setting_type ( name, label, description, datatype ) 
+    VALUES ( 
+        'cat.volume.delete_on_empty',
+        oils_i18n_gettext('cat.volume.delete_on_empty', 'Cat: Delete volume with last copy', 'coust', 'label'),
+        oils_i18n_gettext('cat.volume.delete_on_empty', 'Automatically delete a volume when the last linked copy is deleted', 'coust', 'description'),
+        'bool'
+    );
+
+
+COMMIT;
diff --git a/Open-ILS/src/sql/Pg/upgrade/0565.schema.action-trigger.event_definition.hold-cancel-no-target-notification.sql b/Open-ILS/src/sql/Pg/upgrade/0565.schema.action-trigger.event_definition.hold-cancel-no-target-notification.sql
new file mode 100644 (file)
index 0000000..63a99c5
--- /dev/null
@@ -0,0 +1,39 @@
+-- Evergreen DB patch 0565.schema.action-trigger.event_definition.hold-cancel-no-target-notification.sql
+--
+-- New action trigger event definition: Hold Cancelled (No Target) Email Notification
+--
+BEGIN;
+
+-- check whether patch can be applied
+SELECT evergreen.upgrade_deps_block_check('0565', :eg_version);
+
+INSERT INTO action_trigger.event_definition (id, active, owner, name, hook, validator, reactor, delay, delay_field, group_field, template)
+    VALUES (38, FALSE, 1, 
+        'Hold Cancelled (No Target) Email Notification', 
+        'hold_request.cancel.expire_no_target', 
+        'HoldIsCancelled', 'SendEmail', '30 minutes', 'cancel_time', 'usr',
+$$
+[%- USE date -%]
+[%- user = target.0.usr -%]
+To: [%- params.recipient_email || user.email %]
+From: [%- params.sender_email || default_sender %]
+Subject: Hold Request Cancelled
+
+Dear [% user.family_name %], [% user.first_given_name %]
+The following holds were cancelled because no items were found to fullfil the hold.
+
+[% FOR hold IN target %]
+    Title: [% hold.bib_rec.bib_record.simple_record.title %]
+    Author: [% hold.bib_rec.bib_record.simple_record.author %]
+    Library: [% hold.pickup_lib.name %]
+    Request Date: [% date.format(helpers.format_date(hold.rrequest_time), '%Y-%m-%d') %]
+[% END %]
+
+$$);
+
+INSERT INTO action_trigger.environment (event_def, path) VALUES
+    (38, 'usr'),
+    (38, 'pickup_lib'),
+    (38, 'bib_rec.bib_record.simple_record');
+
+COMMIT;
diff --git a/Open-ILS/src/sql/Pg/upgrade/0566.schema.unAPI_XMLAGG_cleanup.sql b/Open-ILS/src/sql/Pg/upgrade/0566.schema.unAPI_XMLAGG_cleanup.sql
new file mode 100644 (file)
index 0000000..0bdaed5
--- /dev/null
@@ -0,0 +1,525 @@
+-- Evergreen DB patch XXXX.schema.unAPI_XMLAGG_cleanup.sql
+--
+-- FIXME: insert description of change, if needed
+--
+BEGIN;
+
+
+-- check whether patch can be applied
+SELECT evergreen.upgrade_deps_block_check('0566', :eg_version);
+
+CREATE OR REPLACE FUNCTION unapi.bre ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
+DECLARE
+    me      biblio.record_entry%ROWTYPE;
+    layout  unapi.bre_output_layout%ROWTYPE;
+    xfrm    config.xml_transform%ROWTYPE;
+    ouid    INT;
+    tmp_xml TEXT;
+    top_el  TEXT;
+    output  XML;
+    hxml    XML;
+    axml    XML;
+BEGIN
+
+    SELECT id INTO ouid FROM actor.org_unit WHERE shortname = org;
+
+    IF ouid IS NULL THEN
+        RETURN NULL::XML;
+    END IF;
+
+    IF format = 'holdings_xml' THEN -- the special case
+        output := unapi.holdings_xml( obj_id, ouid, org, depth, includes, slimit, soffset, include_xmlns);
+        RETURN output;
+    END IF;
+
+    SELECT * INTO layout FROM unapi.bre_output_layout WHERE name = format;
+
+    IF layout.name IS NULL THEN
+        RETURN NULL::XML;
+    END IF;
+
+    SELECT * INTO xfrm FROM config.xml_transform WHERE name = layout.transform;
+
+    SELECT * INTO me FROM biblio.record_entry WHERE id = obj_id;
+
+    -- grab SVF if we need them
+    IF ('mra' = ANY (includes)) THEN 
+        axml := unapi.mra(obj_id,NULL,NULL,NULL,NULL);
+    ELSE
+        axml := NULL::XML;
+    END IF;
+
+    -- grab hodlings if we need them
+    IF ('holdings_xml' = ANY (includes)) THEN 
+        hxml := unapi.holdings_xml(obj_id, ouid, org, depth, evergreen.array_remove_item_by_value(includes,'holdings_xml'), slimit, soffset, include_xmlns);
+    ELSE
+        hxml := NULL::XML;
+    END IF;
+
+
+    -- generate our item node
+
+
+    IF format = 'marcxml' THEN
+        tmp_xml := me.marc;
+        IF tmp_xml !~ E'<marc:' THEN -- If we're not using the prefixed namespace in this record, then remove all declarations of it
+           tmp_xml := REGEXP_REPLACE(tmp_xml, ' xmlns:marc="http://www.loc.gov/MARC21/slim"', '', 'g');
+        END IF; 
+    ELSE
+        tmp_xml := oils_xslt_process(me.marc, xfrm.xslt)::XML;
+    END IF;
+
+    top_el := REGEXP_REPLACE(tmp_xml, E'^.*?<((?:\\S+:)?' || layout.holdings_element || ').*$', E'\\1');
+
+    IF axml IS NOT NULL THEN 
+        tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', axml || '</' || top_el || E'>\\1');
+    END IF;
+
+    IF hxml IS NOT NULL THEN -- XXX how do we configure the holdings position?
+        tmp_xml := REGEXP_REPLACE(tmp_xml, '</' || top_el || '>(.*?)$', hxml || '</' || top_el || E'>\\1');
+    END IF;
+
+    IF ('bre.unapi' = ANY (includes)) THEN 
+        output := REGEXP_REPLACE(
+            tmp_xml,
+            '</' || top_el || '>(.*?)',
+            XMLELEMENT(
+                name abbr,
+                XMLATTRIBUTES(
+                    'http://www.w3.org/1999/xhtml' AS xmlns,
+                    'unapi-id' AS class,
+                    'tag:open-ils.org:U2@bre/' || obj_id || '/' || org AS title
+                )
+            )::TEXT || '</' || top_el || E'>\\1'
+        );
+    ELSE
+        output := tmp_xml;
+    END IF;
+
+    output := REGEXP_REPLACE(output::TEXT,E'>\\s+<','><','gs')::XML;
+    RETURN output;
+END;
+$F$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION unapi.holdings_xml (bid BIGINT, ouid INT, org TEXT, depth INT DEFAULT NULL, includes TEXT[] DEFAULT NULL::TEXT[], slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE) RETURNS XML AS $F$
+     SELECT  XMLELEMENT(
+                 name holdings,
+                 XMLATTRIBUTES(
+                    CASE WHEN $8 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
+                    CASE WHEN ('bre' = ANY ($5)) THEN 'tag:open-ils.org:U2@bre/' || $1 || '/' || $3 ELSE NULL END AS id
+                 ),
+                 XMLELEMENT(
+                     name counts,
+                     (SELECT  XMLAGG(XMLELEMENT::XML) FROM (
+                         SELECT  XMLELEMENT(
+                                     name count,
+                                     XMLATTRIBUTES('public' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
+                                 )::text
+                           FROM  asset.opac_ou_record_copy_count($2,  $1)
+                                     UNION
+                         SELECT  XMLELEMENT(
+                                     name count,
+                                     XMLATTRIBUTES('staff' as type, depth, org_unit, coalesce(transcendant,0) as transcendant, available, visible as count, unshadow)
+                                 )::text
+                           FROM  asset.staff_ou_record_copy_count($2, $1)
+                                     ORDER BY 1
+                     )x)
+                 ),
+                 CASE 
+                     WHEN ('bmp' = ANY ($5)) THEN
+                        XMLELEMENT(
+                            name monograph_parts,
+                            (SELECT XMLAGG(bmp) FROM (
+                                SELECT  unapi.bmp( id, 'xml', 'monograph_part', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($5,'bre'), 'holdings_xml'), $3, $4, $6, $7, FALSE)
+                                  FROM  biblio.monograph_part
+                                  WHERE record = $1
+                            )x)
+                        )
+                     ELSE NULL
+                 END,
+                 XMLELEMENT(
+                     name volumes,
+                     (SELECT XMLAGG(acn) FROM (
+                        SELECT  unapi.acn(acn.id,'xml','volume',array_remove_item_by_value( evergreen.array_remove_item_by_value($5,'holdings_xml'),'bre'), $3, $4, $6, $7, FALSE)
+                          FROM  asset.call_number acn
+                          WHERE acn.record = $1
+                                AND EXISTS (
+                                    SELECT  1
+                                      FROM  asset.copy acp
+                                            JOIN actor.org_unit_descendants(
+                                                $2,
+                                                (COALESCE(
+                                                    $4,
+                                                    (SELECT aout.depth
+                                                      FROM  actor.org_unit_type aout
+                                                            JOIN actor.org_unit aou ON (aou.ou_type = aout.id AND aou.id = $2)
+                                                    )
+                                                ))
+                                            ) aoud ON (acp.circ_lib = aoud.id)
+                                      LIMIT 1
+                               )
+                          ORDER BY label_sortkey
+                          LIMIT $6
+                          OFFSET $7
+                     )x)
+                 ),
+                 CASE WHEN ('ssub' = ANY ($5)) THEN 
+                     XMLELEMENT(
+                         name subscriptions,
+                         (SELECT XMLAGG(ssub) FROM (
+                            SELECT  unapi.ssub(id,'xml','subscription','{}'::TEXT[], $3, $4, $6, $7, FALSE)
+                              FROM  serial.subscription
+                              WHERE record_entry = $1
+                        )x)
+                     )
+                 ELSE NULL END,
+                 CASE WHEN ('acp' = ANY ($5)) THEN 
+                     XMLELEMENT(
+                         name foreign_copies,
+                         (SELECT XMLAGG(acp) FROM (
+                            SELECT  unapi.acp(p.target_copy,'xml','copy','{}'::TEXT[], $3, $4, $6, $7, FALSE)
+                              FROM  biblio.peer_bib_copy_map p
+                                    JOIN asset.copy c ON (p.target_copy = c.id)
+                              WHERE NOT c.deleted AND peer_record = $1
+                        )x)
+                     )
+                 ELSE NULL END
+             );
+$F$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION unapi.ssub ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
+        SELECT  XMLELEMENT(
+                    name subscription,
+                    XMLATTRIBUTES(
+                        CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
+                        'tag:open-ils.org:U2@ssub/' || id AS id,
+                        start_date AS start, end_date AS end, expected_date_offset
+                    ),
+                    unapi.aou( owning_lib, $2, 'owning_lib', evergreen.array_remove_item_by_value($4,'ssub'), $5, $6, $7, $8),
+                    XMLELEMENT( name distributions,
+                        CASE 
+                            WHEN ('sdist' = ANY ($4)) THEN
+                                (SELECT XMLAGG(sdist) FROM (
+                                    SELECT  unapi.sdist( id, 'xml', 'distribution', evergreen.array_remove_item_by_value($4,'ssub'), $5, $6, $7, $8, FALSE)
+                                      FROM  serial.distribution
+                                      WHERE subscription = ssub.id
+                                )x)
+                            ELSE NULL
+                        END
+                    )
+                )
+          FROM  serial.subscription ssub
+          WHERE id = $1
+          GROUP BY id, start_date, end_date, expected_date_offset, owning_lib;
+$F$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION unapi.sdist ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
+        SELECT  XMLELEMENT(
+                    name distribution,
+                    XMLATTRIBUTES(
+                        CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
+                        'tag:open-ils.org:U2@sdist/' || id AS id,
+                       'tag:open-ils.org:U2@acn/' || receive_call_number AS receive_call_number,
+                       'tag:open-ils.org:U2@acn/' || bind_call_number AS bind_call_number,
+                        unit_label_prefix, label, unit_label_suffix, summary_method
+                    ),
+                    unapi.aou( holding_lib, $2, 'holding_lib', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8),
+                    CASE WHEN subscription IS NOT NULL AND ('ssub' = ANY ($4)) THEN unapi.ssub( subscription, 'xml', 'subscription', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE) ELSE NULL END,
+                    XMLELEMENT( name streams,
+                        CASE 
+                            WHEN ('sstr' = ANY ($4)) THEN
+                                (SELECT XMLAGG(sstr) FROM (
+                                    SELECT  unapi.sstr( id, 'xml', 'stream', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE)
+                                      FROM  serial.stream
+                                      WHERE distribution = sdist.id
+                                )x)
+                            ELSE NULL
+                        END
+                    ),
+                    XMLELEMENT( name summaries,
+                        CASE 
+                            WHEN ('ssum' = ANY ($4)) THEN
+                                (SELECT XMLAGG(sbsum) FROM (
+                                    SELECT  unapi.sbsum( id, 'xml', 'serial_summary', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE)
+                                      FROM  serial.basic_summary
+                                      WHERE distribution = sdist.id
+                                )x)
+                            ELSE NULL
+                        END,
+                        CASE 
+                            WHEN ('ssum' = ANY ($4)) THEN
+                                (SELECT XMLAGG(sisum) FROM (
+                                    SELECT  unapi.sisum( id, 'xml', 'serial_summary', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE)
+                                      FROM  serial.index_summary
+                                      WHERE distribution = sdist.id
+                                )x)
+                            ELSE NULL
+                        END,
+                        CASE 
+                            WHEN ('ssum' = ANY ($4)) THEN
+                                (SELECT XMLAGG(sssum) FROM (
+                                    SELECT  unapi.sssum( id, 'xml', 'serial_summary', evergreen.array_remove_item_by_value($4,'sdist'), $5, $6, $7, $8, FALSE)
+                                      FROM  serial.supplement_summary
+                                      WHERE distribution = sdist.id
+                                )x)
+                            ELSE NULL
+                        END
+                    )
+                )
+          FROM  serial.distribution sdist
+          WHERE id = $1
+          GROUP BY id, label, unit_label_prefix, unit_label_suffix, holding_lib, summary_method, subscription, receive_call_number, bind_call_number;
+$F$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION unapi.sstr ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
+    SELECT  XMLELEMENT(
+                name stream,
+                XMLATTRIBUTES(
+                    CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
+                    'tag:open-ils.org:U2@sstr/' || id AS id,
+                    routing_label
+                ),
+                CASE WHEN distribution IS NOT NULL AND ('sdist' = ANY ($4)) THEN unapi.sssum( distribution, 'xml', 'distribtion', evergreen.array_remove_item_by_value($4,'sstr'), $5, $6, $7, $8, FALSE) ELSE NULL END,
+                XMLELEMENT( name items,
+                    CASE 
+                        WHEN ('sitem' = ANY ($4)) THEN
+                            (SELECT XMLAGG(sitem) FROM (
+                                SELECT  unapi.sitem( id, 'xml', 'serial_item', evergreen.array_remove_item_by_value($4,'sstr'), $5, $6, $7, $8, FALSE)
+                                  FROM  serial.item
+                                  WHERE stream = sstr.id
+                            )x)
+                        ELSE NULL
+                    END
+                )
+            )
+      FROM  serial.stream sstr
+      WHERE id = $1
+      GROUP BY id, routing_label, distribution;
+$F$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION unapi.siss ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
+    SELECT  XMLELEMENT(
+                name issuance,
+                XMLATTRIBUTES(
+                    CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
+                    'tag:open-ils.org:U2@siss/' || id AS id,
+                    create_date, edit_date, label, date_published,
+                    holding_code, holding_type, holding_link_id
+                ),
+                CASE WHEN subscription IS NOT NULL AND ('ssub' = ANY ($4)) THEN unapi.ssub( subscription, 'xml', 'subscription', evergreen.array_remove_item_by_value($4,'siss'), $5, $6, $7, $8, FALSE) ELSE NULL END,
+                XMLELEMENT( name items,
+                    CASE 
+                        WHEN ('sitem' = ANY ($4)) THEN
+                            (SELECT XMLAGG(sitem) FROM (
+                                SELECT  unapi.sitem( id, 'xml', 'serial_item', evergreen.array_remove_item_by_value($4,'siss'), $5, $6, $7, $8, FALSE)
+                                  FROM  serial.item
+                                  WHERE issuance = sstr.id
+                            )x)
+                        ELSE NULL
+                    END
+                )
+            )
+      FROM  serial.issuance sstr
+      WHERE id = $1
+      GROUP BY id, create_date, edit_date, label, date_published, holding_code, holding_type, holding_link_id, subscription;
+$F$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION unapi.sitem ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
+        SELECT  XMLELEMENT(
+                    name serial_item,
+                    XMLATTRIBUTES(
+                        CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
+                        'tag:open-ils.org:U2@sitem/' || id AS id,
+                        'tag:open-ils.org:U2@siss/' || issuance AS issuance,
+                        date_expected, date_received
+                    ),
+                    CASE WHEN issuance IS NOT NULL AND ('siss' = ANY ($4)) THEN unapi.siss( issuance, $2, 'issuance', evergreen.array_remove_item_by_value($4,'sitem'), $5, $6, $7, $8, FALSE) ELSE NULL END,
+                    CASE WHEN stream IS NOT NULL AND ('sstr' = ANY ($4)) THEN unapi.sstr( stream, $2, 'stream', evergreen.array_remove_item_by_value($4,'sitem'), $5, $6, $7, $8, FALSE) ELSE NULL END,
+                    CASE WHEN unit IS NOT NULL AND ('sunit' = ANY ($4)) THEN unapi.sunit( stream, $2, 'serial_unit', evergreen.array_remove_item_by_value($4,'sitem'), $5, $6, $7, $8, FALSE) ELSE NULL END,
+                    CASE WHEN uri IS NOT NULL AND ('auri' = ANY ($4)) THEN unapi.auri( uri, $2, 'uri', evergreen.array_remove_item_by_value($4,'sitem'), $5, $6, $7, $8, FALSE) ELSE NULL END
+--                    XMLELEMENT( name notes,
+--                        CASE 
+--                            WHEN ('acpn' = ANY ($4)) THEN
+--                                (SELECT XMLAGG(acpn) FROM (
+--                                    SELECT  unapi.acpn( id, 'xml', 'copy_note', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8)
+--                                      FROM  asset.copy_note
+--                                      WHERE owning_copy = cp.id AND pub
+--                                )x)
+--                            ELSE NULL
+--                        END
+--                    )
+                )
+          FROM  serial.item sitem
+          WHERE id = $1;
+$F$ LANGUAGE SQL;
+
+
+CREATE OR REPLACE FUNCTION unapi.bmp ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
+        SELECT  XMLELEMENT(
+                    name monograph_part,
+                    XMLATTRIBUTES(
+                        CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
+                        'tag:open-ils.org:U2@bmp/' || id AS id,
+                        id AS ident,
+                        label,
+                        label_sortkey,
+                        'tag:open-ils.org:U2@bre/' || record AS record
+                    ),
+                    CASE 
+                        WHEN ('acp' = ANY ($4)) THEN
+                            XMLELEMENT( name copies,
+                                (SELECT XMLAGG(acp) FROM (
+                                    SELECT  unapi.acp( cp.id, 'xml', 'copy', evergreen.array_remove_item_by_value($4,'bmp'), $5, $6, $7, $8, FALSE)
+                                      FROM  asset.copy cp
+                                            JOIN asset.copy_part_map cpm ON (cpm.target_copy = cp.id)
+                                      WHERE cpm.part = $1
+                                      ORDER BY COALESCE(cp.copy_number,0), cp.barcode
+                                      LIMIT $7
+                                      OFFSET $8
+                                )x)
+                            )
+                        ELSE NULL
+                    END,
+                    CASE WHEN ('bre' = ANY ($4)) THEN unapi.bre( record, 'marcxml', 'record', evergreen.array_remove_item_by_value($4,'bmp'), $5, $6, $7, $8, FALSE) ELSE NULL END
+                )
+          FROM  biblio.monograph_part
+          WHERE id = $1
+          GROUP BY id, label, label_sortkey, record;
+$F$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION unapi.acp ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
+        SELECT  XMLELEMENT(
+                    name copy,
+                    XMLATTRIBUTES(
+                        CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
+                        'tag:open-ils.org:U2@acp/' || id AS id,
+                        create_date, edit_date, copy_number, circulate, deposit,
+                        ref, holdable, deleted, deposit_amount, price, barcode,
+                        circ_modifier, circ_as_type, opac_visible
+                    ),
+                    unapi.ccs( status, $2, 'status', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE),
+                    unapi.acl( location, $2, 'location', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE),
+                    unapi.aou( circ_lib, $2, 'circ_lib', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8),
+                    unapi.aou( circ_lib, $2, 'circlib', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8),
+                    CASE WHEN ('acn' = ANY ($4)) THEN unapi.acn( call_number, $2, 'call_number', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE) ELSE NULL END,
+                    XMLELEMENT( name copy_notes,
+                        CASE 
+                            WHEN ('acpn' = ANY ($4)) THEN
+                                (SELECT XMLAGG(acpn) FROM (
+                                    SELECT  unapi.acpn( id, 'xml', 'copy_note', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE)
+                                      FROM  asset.copy_note
+                                      WHERE owning_copy = cp.id AND pub
+                                )x)
+                            ELSE NULL
+                        END
+                    ),
+                    XMLELEMENT( name statcats,
+                        CASE 
+                            WHEN ('ascecm' = ANY ($4)) THEN
+                                (SELECT XMLAGG(ascecm) FROM (
+                                    SELECT  unapi.ascecm( stat_cat_entry, 'xml', 'statcat', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE)
+                                      FROM  asset.stat_cat_entry_copy_map
+                                      WHERE owning_copy = cp.id
+                                )x)
+                            ELSE NULL
+                        END
+                    ),
+                    XMLELEMENT( name foreign_records,
+                        CASE
+                            WHEN ('bre' = ANY ($4)) THEN
+                                (SELECT XMLAGG(bre) FROM (
+                                    SELECT  unapi.bre(peer_record,'marcxml','record','{}'::TEXT[], $5, $6, $7, $8, FALSE)
+                                      FROM  biblio.peer_bib_copy_map
+                                      WHERE target_copy = cp.id
+                                )x)
+                            ELSE NULL
+                        END
+
+                    ),
+                    CASE 
+                        WHEN ('bmp' = ANY ($4)) THEN
+                            XMLELEMENT( name monograph_parts,
+                                (SELECT XMLAGG(bmp) FROM (
+                                    SELECT  unapi.bmp( part, 'xml', 'monograph_part', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE)
+                                      FROM  asset.copy_part_map
+                                      WHERE target_copy = cp.id
+                                )x)
+                            )
+                        ELSE NULL
+                    END
+                )
+          FROM  asset.copy cp
+          WHERE id = $1
+          GROUP BY id, status, location, circ_lib, call_number, create_date, edit_date, copy_number, circulate, deposit, ref, holdable, deleted, deposit_amount, price, barcode, circ_modifier, circ_as_type, opac_visible;
+$F$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION unapi.sunit ( obj_id BIGINT, format TEXT,  ename TEXT, includes TEXT[], org TEXT, depth INT DEFAULT NULL, slimit INT DEFAULT NULL, soffset INT DEFAULT NULL, include_xmlns BOOL DEFAULT TRUE ) RETURNS XML AS $F$
+        SELECT  XMLELEMENT(
+                    name serial_unit,
+                    XMLATTRIBUTES(
+                        CASE WHEN $9 THEN 'http://open-ils.org/spec/holdings/v1' ELSE NULL END AS xmlns,
+                        'tag:open-ils.org:U2@acp/' || id AS id,
+                        create_date, edit_date, copy_number, circulate, deposit,
+                        ref, holdable, deleted, deposit_amount, price, barcode,
+                        circ_modifier, circ_as_type, opac_visible, status_changed_time,
+                        floating, mint_condition, detailed_contents, sort_key, summary_contents, cost 
+                    ),
+                    unapi.ccs( status, $2, 'status', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($4,'acp'),'sunit'), $5, $6, $7, $8, FALSE),
+                    unapi.acl( location, $2, 'location', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($4,'acp'),'sunit'), $5, $6, $7, $8, FALSE),
+                    unapi.aou( circ_lib, $2, 'circ_lib', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($4,'acp'),'sunit'), $5, $6, $7, $8),
+                    unapi.aou( circ_lib, $2, 'circlib', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($4,'acp'),'sunit'), $5, $6, $7, $8),
+                    CASE WHEN ('acn' = ANY ($4)) THEN unapi.acn( call_number, $2, 'call_number', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE) ELSE NULL END,
+                    XMLELEMENT( name copy_notes,
+                        CASE 
+                            WHEN ('acpn' = ANY ($4)) THEN
+                                (SELECT XMLAGG(acpn) FROM (
+                                    SELECT  unapi.acpn( id, 'xml', 'copy_note', evergreen.array_remove_item_by_value( evergreen.array_remove_item_by_value($4,'acp'),'sunit'), $5, $6, $7, $8, FALSE)
+                                      FROM  asset.copy_note
+                                      WHERE owning_copy = cp.id AND pub
+                                )x)
+                            ELSE NULL
+                        END
+                    ),
+                    XMLELEMENT( name statcats,
+                        CASE 
+                            WHEN ('ascecm' = ANY ($4)) THEN
+                                (SELECT XMLAGG(ascecm) FROM (
+                                    SELECT  unapi.ascecm( stat_cat_entry, 'xml', 'statcat', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE)
+                                      FROM  asset.stat_cat_entry_copy_map
+                                      WHERE owning_copy = cp.id
+                                )x)
+                            ELSE NULL
+                        END
+                    ),
+                    XMLELEMENT( name foreign_records,
+                        CASE
+                            WHEN ('bre' = ANY ($4)) THEN
+                                (SELECT XMLAGG(bre) FROM (
+                                    SELECT  unapi.bre(peer_record,'marcxml','record','{}'::TEXT[], $5, $6, $7, $8, FALSE)
+                                      FROM  biblio.peer_bib_copy_map
+                                      WHERE target_copy = cp.id
+                                )x)
+                            ELSE NULL
+                        END
+
+                    ),
+                    CASE 
+                        WHEN ('bmp' = ANY ($4)) THEN
+                            XMLELEMENT( name monograph_parts,
+                                (SELECT XMLAGG(bmp) FROM (
+                                    SELECT  unapi.bmp( part, 'xml', 'monograph_part', evergreen.array_remove_item_by_value($4,'acp'), $5, $6, $7, $8, FALSE)
+                                      FROM  asset.copy_part_map
+                                      WHERE target_copy = cp.id
+                                )x)
+                            )
+                        ELSE NULL
+                    END
+                )
+          FROM  serial.unit cp
+          WHERE id = $1
+          GROUP BY  id, status, location, circ_lib, call_number, create_date, edit_date, copy_number, circulate, floating, mint_condition,
+                    deposit, ref, holdable, deleted, deposit_amount, price, barcode, circ_modifier, circ_as_type, opac_visible, status_changed_time, detailed_contents, sort_key, summary_contents, cost;
+$F$ LANGUAGE SQL;
+
+COMMIT;
+
diff --git a/Open-ILS/src/sql/Pg/upgrade/0567.data.ou_setting_generate_overdue_on_lost.sql b/Open-ILS/src/sql/Pg/upgrade/0567.data.ou_setting_generate_overdue_on_lost.sql
new file mode 100644 (file)
index 0000000..82c9903
--- /dev/null
@@ -0,0 +1,24 @@
+-- Evergreen DB patch XXXX.data.ou_setting_generate_overdue_on_lost.sql.sql
+BEGIN;
+
+-- check whether patch can be applied
+SELECT evergreen.upgrade_deps_block_check('0567', :eg_version);
+
+INSERT INTO config.org_unit_setting_type ( name, label, description, datatype ) VALUES (
+    'circ.lost.generate_overdue_on_checkin',
+    oils_i18n_gettext( 
+        'circ.lost.generate_overdue_on_checkin',
+        'Circ:  Lost Checkin Generates New Overdues',
+        'coust',
+        'label'
+    ),
+    oils_i18n_gettext( 
+        'circ.lost.generate_overdue_on_checkin',
+        'Enabling this setting causes retroactive creation of not-yet-existing overdue fines on lost item checkin, up to the point of checkin time (or max fines is reached).  This is different than "restore overdue on lost", because it only creates new overdue fines.  Use both settings together to get the full complement of overdue fines for a lost item',
+        'coust',
+        'label'
+    ),
+    'bool'
+);
+
+COMMIT;
diff --git a/Open-ILS/src/sql/Pg/upgrade/0568.schema.cache_visibility_speed_boost.sql b/Open-ILS/src/sql/Pg/upgrade/0568.schema.cache_visibility_speed_boost.sql
new file mode 100644 (file)
index 0000000..3e9c56e
--- /dev/null
@@ -0,0 +1,218 @@
+-- Evergreen DB patch 0568.schema.cache_visibility_speed_boost.sql
+--
+BEGIN;
+
+
+-- check whether patch can be applied
+SELECT evergreen.upgrade_deps_block_check('0568', :eg_version);
+
+CREATE OR REPLACE FUNCTION asset.cache_copy_visibility () RETURNS TRIGGER as $func$
+DECLARE
+    add_front       TEXT;
+    add_back        TEXT;
+    add_base_query  TEXT;
+    add_peer_query  TEXT;
+    remove_query    TEXT;
+    do_add          BOOLEAN := false;
+    do_remove       BOOLEAN := false;
+BEGIN
+    add_base_query := $$
+        SELECT  cp.id, cp.circ_lib, cn.record, cn.id AS call_number, cp.location, cp.status
+          FROM  asset.copy cp
+                JOIN asset.call_number cn ON (cn.id = cp.call_number)
+                JOIN actor.org_unit a ON (cp.circ_lib = a.id)
+                JOIN asset.copy_location cl ON (cp.location = cl.id)
+                JOIN config.copy_status cs ON (cp.status = cs.id)
+                JOIN biblio.record_entry b ON (cn.record = b.id)
+          WHERE NOT cp.deleted
+                AND NOT cn.deleted
+                AND NOT b.deleted
+                AND cs.opac_visible
+                AND cl.opac_visible
+                AND cp.opac_visible
+                AND a.opac_visible
+    $$;
+    add_peer_query := $$
+        SELECT  cp.id, cp.circ_lib, pbcm.peer_record AS record, NULL AS call_number, cp.location, cp.status
+          FROM  asset.copy cp
+                JOIN biblio.peer_bib_copy_map pbcm ON (pbcm.target_copy = cp.id)
+                JOIN actor.org_unit a ON (cp.circ_lib = a.id)
+                JOIN asset.copy_location cl ON (cp.location = cl.id)
+                JOIN config.copy_status cs ON (cp.status = cs.id)
+          WHERE NOT cp.deleted
+                AND cs.opac_visible
+                AND cl.opac_visible
+                AND cp.opac_visible
+                AND a.opac_visible
+    $$;
+    add_front := $$
+        INSERT INTO asset.opac_visible_copies (copy_id, circ_lib, record)
+          SELECT id, circ_lib, record FROM (
+    $$;
+    add_back := $$
+        ) AS x
+    $$;
+    remove_query := $$ DELETE FROM asset.opac_visible_copies WHERE copy_id IN ( SELECT id FROM asset.copy WHERE $$;
+
+    IF TG_TABLE_NAME = 'peer_bib_copy_map' THEN
+        IF TG_OP = 'INSERT' THEN
+            add_peer_query := add_peer_query || ' AND cp.id = ' || NEW.target_copy || ' AND pbcm.record = ' || NEW.peer_record;
+            EXECUTE add_front || add_peer_query || add_back;
+            RETURN NEW;
+        ELSE
+            remove_query := 'DELETE FROM asset.opac_visible_copies WHERE copy_id = ' || OLD.target_copy || ' AND record = ' || OLD.peer_record || ';';
+            EXECUTE remove_query;
+            RETURN OLD;
+        END IF;
+    END IF;
+
+    IF TG_OP = 'INSERT' THEN
+
+        IF TG_TABLE_NAME IN ('copy', 'unit') THEN
+            add_base_query := add_base_query || ' AND cp.id = ' || NEW.id;
+            EXECUTE add_front || add_base_query || add_back;
+        END IF;
+
+        RETURN NEW;
+
+    END IF;
+
+    -- handle items first, since with circulation activity
+    -- their statuses change frequently
+    IF TG_TABLE_NAME IN ('copy', 'unit') THEN
+
+        IF OLD.location    <> NEW.location OR
+           OLD.call_number <> NEW.call_number OR
+           OLD.status      <> NEW.status OR
+           OLD.circ_lib    <> NEW.circ_lib THEN
+            -- any of these could change visibility, but
+            -- we'll save some queries and not try to calculate
+            -- the change directly
+            do_remove := true;
+            do_add := true;
+        ELSE
+
+            IF OLD.deleted <> NEW.deleted THEN
+                IF NEW.deleted THEN
+                    do_remove := true;
+                ELSE
+                    do_add := true;
+                END IF;
+            END IF;
+
+            IF OLD.opac_visible <> NEW.opac_visible THEN
+                IF OLD.opac_visible THEN
+                    do_remove := true;
+                ELSIF NOT do_remove THEN -- handle edge case where deleted item
+                                        -- is also marked opac_visible
+                    do_add := true;
+                END IF;
+            END IF;
+
+        END IF;
+
+        IF do_remove THEN
+            DELETE FROM asset.opac_visible_copies WHERE copy_id = NEW.id;
+        END IF;
+        IF do_add THEN
+            add_base_query := add_base_query || ' AND cp.id = ' || NEW.id;
+            add_peer_query := add_peer_query || ' AND cp.id = ' || NEW.id;
+            EXECUTE add_front || add_base_query || ' UNION ' || add_peer_query || add_back;
+        END IF;
+
+        RETURN NEW;
+
+    END IF;
+
+    IF TG_TABLE_NAME IN ('call_number', 'record_entry') THEN -- these have a 'deleted' column
+        IF OLD.deleted AND NEW.deleted THEN -- do nothing
+
+            RETURN NEW;
+        ELSIF NEW.deleted THEN -- remove rows
+            IF TG_TABLE_NAME = 'call_number' THEN
+                DELETE FROM asset.opac_visible_copies WHERE copy_id IN (SELECT id FROM asset.copy WHERE call_number = NEW.id);
+            ELSIF TG_TABLE_NAME = 'record_entry' THEN
+                DELETE FROM asset.opac_visible_copies WHERE record = NEW.id;
+            END IF;
+            RETURN NEW;
+        ELSIF OLD.deleted THEN -- add rows
+            IF TG_TABLE_NAME = 'call_number' THEN
+                add_base_query := add_base_query || ' AND cn.id = ' || NEW.id;
+                EXECUTE add_front || add_base_query || add_back;
+            ELSIF TG_TABLE_NAME = 'record_entry' THEN
+                add_base_query := add_base_query || ' AND cn.record = ' || NEW.id;
+                add_peer_query := add_peer_query || ' AND pbcm.record = ' || NEW.id;
+                EXECUTE add_front || add_base_query || ' UNION ' || add_peer_query || add_back;
+            END IF;
+            RETURN NEW;
+        END IF;
+    END IF;
+
+    IF TG_TABLE_NAME = 'call_number' THEN
+
+        IF OLD.record <> NEW.record THEN
+            -- call number is linked to different bib
+            remove_query := remove_query || 'call_number = ' || NEW.id || ');';
+            EXECUTE remove_query;
+            add_base_query := add_base_query || ' AND cn.id = ' || NEW.id;
+            EXECUTE add_front || add_base_query || add_back;
+        END IF;
+
+        RETURN NEW;
+
+    END IF;
+
+    IF TG_TABLE_NAME IN ('record_entry') THEN
+        RETURN NEW; -- don't have 'opac_visible'
+    END IF;
+
+    -- actor.org_unit, asset.copy_location, asset.copy_status
+    IF NEW.opac_visible = OLD.opac_visible THEN -- do nothing
+
+        RETURN NEW;
+
+    ELSIF NEW.opac_visible THEN -- add rows
+
+        IF TG_TABLE_NAME = 'org_unit' THEN
+            add_base_query := add_base_query || ' AND cp.circ_lib = ' || NEW.id || ';';
+            add_peer_query := add_peer_query || ' AND cp.circ_lib = ' || NEW.id || ';';
+        ELSIF TG_TABLE_NAME = 'copy_location' THEN
+            add_base_query := add_base_query || ' AND cp.location = ' || NEW.id || ';';
+            add_peer_query := add_peer_query || ' AND cp.location = ' || NEW.id || ';';
+        ELSIF TG_TABLE_NAME = 'copy_status' THEN
+            add_base_query := add_base_query || ' AND cp.status = ' || NEW.id || ';';
+            add_peer_query := add_peer_query || ' AND cp.status = ' || NEW.id || ';';
+        END IF;
+        EXECUTE add_front || add_base_query || ' UNION ' || add_peer_query || add_back;
+    ELSE -- delete rows
+
+        IF TG_TABLE_NAME = 'org_unit' THEN
+            remove_query := 'DELETE FROM asset.opac_visible_copies WHERE circ_lib = ' || NEW.id || ';';
+        ELSIF TG_TABLE_NAME = 'copy_location' THEN
+            remove_query := remove_query || 'location = ' || NEW.id || ');';
+        ELSIF TG_TABLE_NAME = 'copy_status' THEN
+            remove_query := remove_query || 'status = ' || NEW.id || ');';
+        END IF;
+        EXECUTE remove_query;
+    END IF;
+    RETURN NEW;
+END;
+$func$ LANGUAGE PLPGSQL;
+
+COMMIT;
+
index 6437c73..38c7ac8 100644 (file)
@@ -8,7 +8,7 @@ opacjsdir = $(DESTDIR)$(WEBDIR)/opac/common/js
 jsdojodir = $(DESTDIR)$(WEBDIR)/js/dojo
 jsdojoosrfdir = $(DESTDIR)$(WEBDIR)/js/dojo/opensrf
 opacextrasdir = $(DESTDIR)$(WEBDIR)/opac/extras/xsl/
-reportsdir = $(DESTDIR)$(WEBDIR)/reports/
+reportsdir = $(WEBDIR)/reports/
 
 if BUILDILSWEB
 OILSWEB_INST = webcore-install offline-install
@@ -63,7 +63,7 @@ offline-install:
        @echo "Installing offline CGIs to $(CGIDIR)/offline";
        $(MKDIR_P) $(TMP)
        $(MKDIR_P) $(DESTDIR)$(CGIDIR)/offline;
-       $(MKDIR_P) $(datadir)/offline;
+       $(MKDIR_P) $(DESTDIR)$(datadir)/offline;
        perl -pe "s{##CONFIG##}{@sysconfdir@}" < @top_srcdir@/Open-ILS/src/offline/offline.pl > $(TMP)/offline.pl;
        cp $(TMP)/offline.pl $(DESTDIR)$(CGIDIR)/offline/
        chmod +x $(DESTDIR)$(CGIDIR)/offline/offline.pl
index 943fd24..262eaf2 100644 (file)
@@ -82,6 +82,8 @@
                 if (attr == 'opac_visible' && typeof n != 'string')
                     this.setValue(item, 'opac_visible', n ? 't' : 'f');
 
+                if (attr == 'copy_active' && typeof n != 'string')
+                    this.setValue(item, 'copy_active', n ? 't' : 'f');
             };
 
             dojo.addOnUnload( function (event) {
                                                                                                return false;
                                                                                        }
                                                                                  }
+                                                                               },
+                                                                               { name : ccs_strings.COPY_ACTIVE,
+                                                                                 field : "copy_active",
+                                                                                 editor : dojox.grid.editors.bool,
+                                                                                 get : function (row) {
+                                                                                       var r = window.status_data_model.getRow(row);
+                                                                                       if (r) {
+                                                                                               var h = r.copy_active;
+                                                                                               if (h == 't' || h === true) return true;
+                                                                                               return false;
+                                                                                       }
+                                                                                 }
                                                                                }
                                                                        ]
                                                                ]
index 363d21d..c482bb6 100644 (file)
@@ -6,6 +6,7 @@
        "CONFIRM_EXIT_PGT": "There are unsaved modified permission maps. Click OK to save these changes, or Cancel to abandon them.",
        "CONFIRM_EXIT_PPL": "There are unsaved modified permissions. Click OK to save these changes, or Cancel to abandon them.",
        "CONFIRM_UNSAVED_CHANGES": "There are unsaved changes to one or more organization types. Click OK to save these changes, or Cancel to abandon them.",
+       "COPY_ACTIVE": "Sets copy active",
        "ERROR_CALLING_METHOD_AOUT": "Problem calling method to create child organization type",
        "ERROR_CALLING_METHOD_CAM": "Problem calling method to create new ${0}",
        "ERROR_CALLING_METHOD_CCS": "Problem calling method to create new copy status",
index 43864d3..be1c111 100644 (file)
@@ -593,6 +593,10 @@ function uEditDrawSettingRow(tbody, dividerRow, template, stype) {
     dojo.connect(cb, 'onChange', function(newVal) { userSettingsToUpdate[stype.name()] = newVal; });
     tbody.insertBefore(row, dividerRow.nextSibling);
     openils.Util.show(row, 'table-row');
+
+    if(stype.name() == 'circ.collections.exempt') {
+        checkCollectionsExemptPerm(cb);
+    }
 }
 
 function uEditUpdateUserSettings(userId) {
@@ -894,6 +898,22 @@ function checkClaimsNoCheckoutCountPerm() {
     );
 }
 
+var collectExemptCBox;
+function checkCollectionsExemptPerm(cbox) {
+    if(cbox) collectExemptCBox = cbox;
+    new openils.User().getPermOrgList(
+        'UPDATE_PATRON_COLLECTIONS_EXEMPT',
+        function(orgList) { 
+            if(orgList.indexOf(patron.home_ou()) == -1) 
+                collectExemptCBox.attr('disabled', true);
+            else
+                collectExemptCBox.attr('disabled', false);
+        },
+        true, 
+        true
+    );
+}
+
 
 function attachWidgetEvents(fmcls, fmfield, widget) {
 
@@ -1085,6 +1105,7 @@ function attachWidgetEvents(fmcls, fmfield, widget) {
                     function(newVal) { 
                         checkClaimsReturnCountPerm(); 
                         checkClaimsNoCheckoutCountPerm();
+                        checkCollectionsExemptPerm();
                     }
                 );
                 return;
index 3011c4a..3c4c52d 100644 (file)
@@ -340,8 +340,7 @@ var FETCH_ADV_ISBN_RIDS                     = "open-ils.search:open-ils.search.biblio.isbn:1";
 var FETCH_ADV_ISSN_RIDS                        = "open-ils.search:open-ils.search.biblio.issn:1";
 var FETCH_ADV_TCN_RIDS                 = "open-ils.search:open-ils.search.biblio.tcn";
 var FETCH_CNBROWSE                             = 'open-ils.search:open-ils.search.callnumber.browse';
-var FETCH_CONTAINERS                           = 'open-ils.actor:open-ils.actor.container.retrieve_by_class';
-var FETCH_CONTAINERS                           = 'open-ils.actor:open-ils.actor.container.retrieve_by_class';
+var FETCH_CONTAINERS                           = 'open-ils.actor:open-ils.actor.container.retrieve_by_class.authoritative';
 var CREATE_CONTAINER                           = 'open-ils.actor:open-ils.actor.container.create';
 var DELETE_CONTAINER                           = 'open-ils.actor:open-ils.actor.container.full_delete';
 var CREATE_CONTAINER_ITEM              = 'open-ils.actor:open-ils.actor.container.item.create';
diff --git a/Open-ILS/web/opac/images/openlibrary.gif b/Open-ILS/web/opac/images/openlibrary.gif
new file mode 100644 (file)
index 0000000..9ac053d
Binary files /dev/null and b/Open-ILS/web/opac/images/openlibrary.gif differ
index 893059a..ffbce22 100644 (file)
 <!ENTITY staff.browse_list.circulate "Circulate">
 <!ENTITY staff.browse_list.copy_number "Copy Number">
 <!ENTITY staff.browse_list.create_date "Creation Date">
+<!ENTITY staff.browse_list.active_date "Active Date">
 <!ENTITY staff.browse_list.creator "Creator">
 <!ENTITY staff.browse_list.deposit "Deposit">
 <!ENTITY staff.browse_list.deposit_amount "Deposit Amount">
 <!ENTITY staff.cat.opac.mark_for_hold_transfer.accesskey "">
 <!ENTITY staff.cat.opac.mark_for_hold_transfer.label "Mark as Title Hold Transfer Destination">
 <!ENTITY staff.cat.opac.transfer_title_holds.accesskey "">
-<!ENTITY staff.cat.opac.transfer_title_holds.label "Transfer Title Holds">
+<!ENTITY staff.cat.opac.transfer_title_holds.label "Transfer All Title Holds">
 <!ENTITY staff.cat.opac.delete_record.accesskey "D">
 <!ENTITY staff.cat.opac.delete_record.label "Delete Record">
 <!ENTITY staff.cat.opac.undelete_record.accesskey "U">
 <!ENTITY staff.main.menu.admin.server_admin.conify.copy_status.label "Copy Statuses">
 <!ENTITY staff.main.menu.admin.server_admin.conify.marc_record_attrs.label "MARC Record Attributes">
 <!ENTITY staff.main.menu.admin.server_admin.conify.coded_value_maps.label "MARC Coded Value Maps">
+<!ENTITY staff.main.menu.admin.server_admin.conify.metabib_field.label "MARC Search/Facet Fields">
 <!ENTITY staff.main.menu.admin.server_admin.conify.acn_prefix.label "Call Number Prefixes">
 <!ENTITY staff.main.menu.admin.server_admin.conify.acn_suffix.label "Call Number Suffixes">
 <!ENTITY staff.main.menu.admin.server_admin.conify.billing_type.label "Billing Types">
 <!ENTITY staff.cat.copy_summary.created.label "Created:">
 <!ENTITY staff.cat.copy_summary.edited.label "Edited:">
 <!ENTITY staff.cat.copy_summary.age_protect.label "Age Protect:">
+<!ENTITY staff.cat.copy_summary.active_date.label "Active Date:">
 <!ENTITY staff.cat.copy_summary.total_circs.label "Total Circulations:">
 <!ENTITY staff.cat.copy_summary.alternate_view.label "Alternate View">
 <!ENTITY staff.cat.copy_summary.alternate_view.accesskey "">
 <!ENTITY staff.cat.volume_editor.cancel.accesskey "C">
 <!ENTITY staff.cat.volume_editor.automerge.label "Auto-Merge on Volume Collision">
 <!ENTITY staff.cat.volume_editor.automerge.accesskey "A">
+<!ENTITY staff.cat.volume_editor.automerge.description "If two or more volumes for the same bib record and library are given the same call number label (and prefix/suffix), then merge them (including items they contain) into a single volume if checked.  Otherwise, report an error.">
 <!ENTITY staff.cat.volume_editor.owning_lib "Owning lib">
 <!ENTITY staff.cat.volume_editor.classification "Classification">
 <!ENTITY staff.cat.volume_editor.prefix "Prefix">
 <!ENTITY staff.circ.alternate_copy_summary.Copy_Location.label "Copy Location">
 <!ENTITY staff.circ.alternate_copy_summary.Renewal_Type.label "Renewal Type">
 <!ENTITY staff.circ.alternate_copy_summary.Date_Created.label "Date Created">
+<!ENTITY staff.circ.alternate_copy_summary.Date_Active.label "Date Active">
 <!ENTITY staff.circ.alternate_copy_summary.Status_Changed_Time.label "Status Changed">
 <!ENTITY staff.circ.alternate_copy_summary.Due_Date.label "Due Date">
 <!ENTITY staff.circ.alternate_copy_summary.Edition.label "Edition">
index 8203ce4..cac50e0 100644 (file)
@@ -494,6 +494,7 @@ Please see a librarian to renew your account.">
 <!ENTITY rdetail.cn.location "Location">
 <!ENTITY rdetail.cn.hold.age "Age Hold Protection">
 <!ENTITY rdetail.cn.genesis "Create Date">
+<!ENTITY rdetail.cn.active "Active Date">
 <!ENTITY rdetail.cn.holdable "Holdable">
 <!ENTITY rdetail.cn.due "Due Date">
 <!ENTITY rdetail.cn.more "more info...">
index 116f1fb..6dc5120 100644 (file)
@@ -15,6 +15,7 @@
                                                                        <td>&rdetail.cn.location;</td>
                                                                        <td name='age_protect_label' class='hide_me'>&rdetail.cn.hold.age;</td>
                                                                        <td name='create_date_label' class='hide_me'>&rdetail.cn.genesis;</td>
+                                    <td name='active_date_label' class='hide_me'>&rdetail.cn.active;</td>
                                                                        <td name='holdable_label' class='hide_me'>&rdetail.cn.holdable;</td>
                                                                        <td name='due_date_label' class='hide_me'>&rdetail.cn.due;</td>
                                                                </tr>
@@ -33,6 +34,7 @@
                                                                        <td name='location'> </td>
                                                                        <td name='age_protect_value' class='hide_me'>&rdetail.cn.disabled;</td>
                                                                        <td name='create_date_value' class='hide_me'> </td>
+                                    <td name='active_date_value' class='hide_me'> </td>
        
                                                                        <td name='copy_holdable_td' class='hide_me'>
                                                                                <span name='copy_is_holdable'> </span>
index c4c623a..253e671 100644 (file)
@@ -31,6 +31,7 @@ function cpdBuild( contextTbody, contextRow, record, callnumber, orgid, depth, c
                /* unhide before we unhide/clone the parent */
                unHideMe($n(templateRow, 'age_protect_label'));
                unHideMe($n(templateRow, 'create_date_label'));
+        unHideMe($n(templateRow, 'active_date_label'));
                unHideMe($n(templateRow, 'holdable_label'));
        }
 
@@ -205,6 +206,7 @@ function cpdDrawCopies(r) {
                /* unhide before we unhide/clone the parent */
                unHideMe($n(copyrow, 'age_protect_value'));
                unHideMe($n(copyrow, 'create_date_value'));
+        unHideMe($n(copyrow, 'active_date_value'));
                unHideMe($n(copyrow, 'copy_holdable_td'));
        }
 
@@ -343,6 +345,12 @@ function cpdDrawCopy(r) {
                cd = cd.replace(/T.*/, '');
                $n(row, 'create_date_value').appendChild(text(cd));
 
+        var ad = copy.active_date();
+        if(ad) {
+            ad = ad.replace(/T.*/, '');
+            $n(row, 'active_date_value').appendChild(text(ad));
+        }
+
                var yes = $('rdetail.yes').innerHTML;
                var no = $('rdetail.no').innerHTML;
 
index 5b5004a..eae2aa0 100644 (file)
@@ -352,19 +352,20 @@ function loadMarcEditor(recId) {
  */
 function _holdingsDraw(h) {
     holdings = h.getResultObject();
-    if (!holdings) { return null; }
-
-    // Only draw holdings within our OU scope
-    var here = findOrgUnit(getLocation());
-    var entryNum = 0;
-    dojo.forEach(holdings, function (item) {
-        if (orgIsMine(here, findOrgUnit(item.owning_lib()))) {
-            _holdingsDrawMFHD(item, entryNum);
-            entryNum++;
-        }
-    });
 
-    // Populate XUL menus
+    if (holdings) {
+        // Only draw holdings within our OU scope
+        var here = findOrgUnit(getLocation());
+        var entryNum = 0;
+        dojo.forEach(holdings, function (item) {
+            if (orgIsMine(here, findOrgUnit(item.owning_lib()))) {
+                _holdingsDrawMFHD(item, entryNum);
+                entryNum++;
+            }
+        });
+    }
+
+    // Populate (or unpopulate) XUL menus
     if (isXUL()) {
         runEvt('rdetail','MFHDDrawn');
     }
index dfd5455..6a5d8c3 100644 (file)
@@ -5,8 +5,9 @@ var opac_strings = dojo.i18n.getLocalization("openils.opac", "opac");
 var recordsHandled = 0;
 var recordsCache = [];
 var lowHitCount = 4;
-var isbnList = '';
+var isbnList = new Array();
 var googleBooksLink = true;
+var OpenLibraryLinks = true;
 
 var resultFetchAllRecords = false;
 var resultCompiledSearch = null;
@@ -16,7 +17,7 @@ if( findCurrentPage() == MRESULT || findCurrentPage() == RRESULT ) {
        G.evt.result.hitCountReceived.push(resultSetHitInfo);
        G.evt.result.recordReceived.push(resultDisplayRecord, resultAddCopyCounts);
        G.evt.result.copyCountsReceived.push(resultDisplayCopyCounts);
-       G.evt.result.allRecordsReceived.push( function(){unHideMe($('result_info_2'))}, fetchGoogleBooksLink, fetchChiliFreshReviews);
+       G.evt.result.allRecordsReceived.push( function(){unHideMe($('result_info_2'))}, fetchOpenLibraryLinks, fetchGoogleBooksLink, fetchChiliFreshReviews);
 
        attachEvt('result','lowHits',resultLowHits);
        attachEvt('result','zeroHits',resultZeroHits);
@@ -450,16 +451,22 @@ function resultDisplayRecord(rec, pos, is_mr) {
        var r = table.rows[pos + 1];
     var currentISBN = cleanISBN(rec.isbn());
 
-    if (googleBooksLink) {
-           var gbspan = $n(r, "googleBooksLink");
-        if (currentISBN) {
+    if (currentISBN) {
+        isbnList.push(currentISBN);
+        if (OpenLibraryLinks) {
+            var olspan = $n(r, 'openLibraryLink');
+            olspan.setAttribute('name', olspan.getAttribute('name') + 
+                '-' + currentISBN
+            );
+        }
+
+        if (googleBooksLink) {
+            var gbspan = $n(r, "googleBooksLink");
             gbspan.setAttribute(
                 'name',
                 gbspan.getAttribute('name') + '-' + currentISBN
             );
 
-            if (isbnList) isbnList += ', ';
-            isbnList += currentISBN;
         }
     }
 
@@ -676,13 +683,116 @@ function resultBuildFormatIcons( row, rec, is_mr ) {
        }
 }
 
+function fetchOpenLibraryLinks() {
+    if (isbnList.length > 0 && OpenLibraryLinks) {
+        /* OpenLibrary supports a number of different identifiers:
+         * ISBN: isbn:<isbn>
+         * LCCN: lccn:<lccn>
+         * OpenLibrary ID: olid:<openlibrary-ID>
+         *
+         * We'll just fire off ISBNs for now.
+         */
+
+        var isbns = '';
+        dojo.forEach(isbnList, function(isbn) {
+            isbns += 'isbn:' + isbn + '|';
+        });
+        isbns = isbns.replace(/.$/, '');
+    }
+
+    dojo.xhrGet({
+        "url": "/opac/extras/ac/proxy/json/" + isbns,
+        "handleAs": "json",
+        "load": function (data) { renderOpenLibraryLinks(data); }
+    });
+
+}
+
+function renderOpenLibraryLinks(response) {
+    var ol_ebooks = {};
+
+    /* Iterate over each identifier we requested */
+    for (var item_id in response) {
+
+        var isbn = item_id.replace(/^isbn:/, '');
+        /* Iterate over each matching item; OpenLibrary supplies access info:
+         *  * match: "exact" or "similar"
+         *  * status: "full access" or "lendable"
+         */
+        dojo.forEach(response[item_id].items, function(item) {
+            ol_ebooks[isbn] = {};
+            if (item.match == 'exact') {
+                if (item.status == 'full access') {
+                    ol_ebooks[isbn]['exact_full'] = item.itemURL;
+                } else {
+                    ol_ebooks[isbn]['exact_lendable'] = item.itemURL;
+                }
+            } else {
+                if (item.status == 'full access') {
+                    ol_ebooks[isbn]['similar_full'] = item.itemURL;
+                } else {
+                    ol_ebooks[isbn]['similar_lendable'] = item.itemURL;
+                }
+            }
+        });
+
+        /* If there are no books to read or borrow, move on */
+        if (!ol_ebooks[isbn]) {
+            continue;
+        }
+
+        /* Now populate the results page with our ebook goodness*/
+        /* Go for the jugular - exact match with full access */
+        if (ol_ebooks[isbn]['exact_full']) {
+            createOpenLibraryLink(
+                isbn, ol_ebooks[isbn]['exact_full'], 'Read online'
+            );
+            continue;
+        }
+
+        /* Fall back to slightly less palatable options */
+        else if (ol_ebooks[isbn]['exact_lendable']) {
+            createOpenLibraryLink(
+                isbn, ol_ebooks[isbn]['exact_lendable'], 'Borrow online'
+            );
+        }
+
+        if (ol_ebooks[isbn]['similar_full']) {
+            createOpenLibraryLink(
+                isbn, ol_ebooks[isbn]['similar_full'], 'Read similar online'
+            );
+        } else if (ol_ebooks[isbn]['similar_lendable']) {
+            createOpenLibraryLink(
+                isbn, ol_ebooks[isbn]['similar_lendable'], 'Borrow similar online'
+            );
+        }
+    }
+}
+
+function createOpenLibraryLink(isbn, url, text) {
+    var ol_span = $n(document.documentElement, 'openLibraryLink-' + isbn);
+
+    var ol_a_span = dojo.create('a', {
+            "href": url,
+            "class": "classic_link"
+        }, ol_span
+    );
+    dojo.create('img', {
+            "src": "/opac/images/openlibrary.gif"
+        }, ol_a_span
+    );
+    dojo.create('br', null, ol_a_span);
+    ol_a_span.appendChild(dojo.doc.createTextNode(text));
+    dojo.removeClass(ol_span, 'hide_me');
+}
+
 function fetchGoogleBooksLink () {
-    if (isbnList && googleBooksLink) {
+    if (isbnList.length > 0 && googleBooksLink) {
         var scriptElement = document.createElement("script");
         scriptElement.setAttribute("id", "jsonScript");
         scriptElement.setAttribute("src",
             "http://books.google.com/books?bibkeys=" + 
-            escape(isbnList) + "&jscmd=viewapi&callback=unhideGoogleBooksLink");
+            escape(isbnList.join(', ')) + "&jscmd=viewapi&callback=unhideGoogleBooksLink");
         scriptElement.setAttribute("type", "text/javascript");
         // make the request to Google Book Search
         document.documentElement.firstChild.appendChild(scriptElement);
index f937089..d443b9f 100644 (file)
@@ -16,6 +16,7 @@
                                                                <td name='copy_part_label' class='hide_me'>&rdetail.cn.part;</td>
                                                                <td name='age_protect_label' class='hide_me'>&rdetail.cn.hold.age;</td>
                                                                <td name='create_date_label' class='hide_me'>&rdetail.cn.genesis;</td>
+                                <td name='active_date_label' class='hide_me'>&rdetail.cn.active;</td>
                                                                <td name='holdable_label' class='hide_me'>&rdetail.cn.holdable;</td>
                                                                <td name='due_date_label' class='hide_me'>&rdetail.cn.due;</td>
                                                        </tr>
@@ -41,6 +42,7 @@
                                                                <td name='copy_part' class='hide_me'> </td>
                                                                <td name='age_protect_value' class='hide_me'>&rdetail.cn.disabled;</td>
                                                                <td name='create_date_value' class='hide_me'> </td>
+                                <td name='active_date_value' class='hide_me'> </td>
 
                                                                <td name='copy_holdable_td' class='hide_me'>
                                                                        <span name='copy_is_holdable'> </span>
index 7ee942a..03345ef 100644 (file)
@@ -44,6 +44,8 @@
                                     </a>
                                 </td>
 
+                                <!-- Empty span used for creating OpenLibrary links -->
+                                <td rowspan='4' name="openLibraryLink" style="text-align: center; vertical-align: middle; width: 15em;" class="hide_me"></td>
                                 <!-- Copy this td for each copy count appended -->
                                 <td  rowspan='4' nowrap='nowrap' name="copy_count_cell" class='copy_count_cell'> 
                                 </td>
index bd19e7e..e3725a2 100644 (file)
@@ -9,7 +9,7 @@
     <table  jsId="cmGrid"
             style="height: 600px;"
             dojoType="openils.widget.AutoGrid"
-            fieldOrder="['id', 'active', 'grp', 'org_unit', 'copy_circ_lib', 'copy_owning_lib', 'user_home_ou', 'is_renewal', 'juvenile_flag', 'circ_modifier', 'marc_type', 'marc_form', 'marc_bib_level', 'marc_vr_format', 'ref_flag', 'usr_age_lower_bound', 'usr_age_upper_bound', 'circulate', 'duration_rule', 'renewals', 'hard_due_date', 'recurring_fine_rule', 'grace_period', 'max_fine_rule', 'available_copy_hold_ratio', 'total_copy_hold_ratio', 'script_test']"
+            fieldOrder="['id', 'active', 'grp', 'org_unit', 'copy_circ_lib', 'copy_owning_lib', 'user_home_ou', 'is_renewal', 'juvenile_flag', 'circ_modifier', 'marc_type', 'marc_form', 'marc_bib_level', 'marc_vr_format', 'ref_flag', 'usr_age_lower_bound', 'usr_age_upper_bound', 'item_age', 'circulate', 'duration_rule', 'renewals', 'hard_due_date', 'recurring_fine_rule', 'grace_period', 'max_fine_rule', 'available_copy_hold_ratio', 'total_copy_hold_ratio', 'script_test']"
             defaultCellWidth='"auto"'
             query="{id: '*'}"
             fmClass='ccmm'
diff --git a/Open-ILS/web/templates/default/conify/global/config/metabib_field.tt2 b/Open-ILS/web/templates/default/conify/global/config/metabib_field.tt2
new file mode 100644 (file)
index 0000000..0a811a7
--- /dev/null
@@ -0,0 +1,32 @@
+[% WRAPPER default/base.tt2 %]
+[% ctx.page_title = 'Metabib Field' %]
+<div dojoType="dijit.layout.ContentPane" layoutAlign="client">
+    <div dojoType="dijit.layout.ContentPane" layoutAlign="top" class='oils-header-panel'>
+        <div>Metabib Field</div>
+        <div>
+            <button dojoType='dijit.form.Button' onClick='mbFieldGrid.showCreateDialog()'>New Field</button>
+            <button dojoType='dijit.form.Button' onClick='mbFieldGrid.deleteSelected()'>Delete Selected</button>
+        </div>
+    </div>
+    <div>
+    <table  jsId="mbFieldGrid"
+            dojoType="openils.widget.AutoGrid"
+            fieldOrder="['name', 'label', 'field_class', 'weight', 'format', 'search_field', 'facet_field', 'xpath']"
+            query="{field: '*'}"
+            fmClass='cmf'
+            autoHeight='true'
+            editOnEnter='true'>
+        <thead>
+            <tr><th field='xpath' width='25%'/></tr>
+        </thead>
+    </table>
+</div>
+
+<script type="text/javascript">
+    dojo.require('openils.Util');
+    dojo.require('openils.widget.AutoGrid');
+    openils.Util.addOnLoad( function() { mbFieldGrid.loadAll(); } );
+</script>
+[% END %]
+
+
index 318329d..51a61c0 100644 (file)
@@ -9,7 +9,7 @@ export STAFF_CLIENT_STAMP_ID = $$(/bin/cat build/STAMP_ID)
 
 # from http://closure-compiler.googlecode.com/files/compiler-latest.zip  FIXME: Autotools this?
 export CLOSURE_COMPILER_JAR = ~/closure-compiler/compiler.jar
-XULRUNNER_VERSION=1.9.2.17
+XULRUNNER_VERSION=1.9.2.18
 XULRUNNER_WINFILE=xulrunner-$(XULRUNNER_VERSION).en-US.win32.zip
 XULRUNNER_LINUXFILE=xulrunner-$(XULRUNNER_VERSION).en-US.linux-i686.tar.bz2
 XULRUNNER_URL=http://releases.mozilla.org/pub/mozilla.org/xulrunner/releases/$(XULRUNNER_VERSION)/runtimes/
@@ -179,12 +179,12 @@ needwebdir:
 
 server-xul: needwebdir build
        @echo $@
-       mkdir -p $(WEBDIR)
-       mkdir -p $(WEBDIR)/xul/
+       mkdir -p $(DESTDIR)$(WEBDIR)
+       mkdir -p $(DESTDIR)$(WEBDIR)/xul/
        @echo "STAMP_ID = $(STAFF_CLIENT_STAMP_ID)"
-       @echo "Copying xul into $(WEBDIR)/xul/$(STAFF_CLIENT_STAMP_ID)"
-       mkdir -p "$(WEBDIR)/xul/$(STAFF_CLIENT_STAMP_ID)"
-       cp -R @top_srcdir@/Open-ILS/xul/staff_client/build/server "${WEBDIR}/xul/${STAFF_CLIENT_STAMP_ID}/"
+       @echo "Copying xul into $(DESTDIR)$(WEBDIR)/xul/$(STAFF_CLIENT_STAMP_ID)"
+       mkdir -p "$(DESTDIR)$(WEBDIR)/xul/$(STAFF_CLIENT_STAMP_ID)"
+       cp -R @top_srcdir@/Open-ILS/xul/staff_client/build/server "$(DESTDIR)${WEBDIR}/xul/${STAFF_CLIENT_STAMP_ID}/"
 
 compress-javascript: build
        @echo "Size of build/ before compression = " `du -sh build/`
index 9d54056..228f92b 100644 (file)
@@ -490,7 +490,12 @@ function set_opac() {
                                 item = mfhd_delete_menu.appendItem(label);
                                 item.setAttribute('oncommand','delete_mfhd('+mfhd_details.id+')');
                             }
+                        } else if (g.mfhd) { // clear from previous runs if deleting last MFHD
+                            delete g.mfhd;
                         }
+                        var change_event = document.createEvent("Event");
+                        change_event.initEvent("MFHDChange",false,false);
+                        window.dispatchEvent(change_event);
                     }
                 );
             },
@@ -558,7 +563,7 @@ function create_mfhd() {
             throw(r);
         }
         alert("MFHD record created."); //TODO: better success message
-        //TODO: refresh opac display
+        browser_frame.contentWindow.g.browser.controller.view.cmd_reload.doCommand();
     } catch(E) {
         g.error.standard_unexpected_error_alert("Create MFHD failed", E); //TODO: better error handling
     }
@@ -580,7 +585,7 @@ function delete_mfhd(sre_id) {
             alert(document.getElementById('offlineStrings').getFormattedString('cat.opac.record_deleted.error',  [docid, robj.textcode, robj.desc]) + '\n');
         } else {
             alert(document.getElementById('offlineStrings').getString('cat.opac.record_deleted'));
-            //TODO: refresh opac display
+            browser_frame.contentWindow.g.browser.controller.view.cmd_reload.doCommand();
         }
     }
 }
index 6743d95..d02c172 100644 (file)
@@ -145,6 +145,7 @@ var api = {
     'FM_AHR_RESET' : { 'app' : 'open-ils.circ', 'method' : 'open-ils.circ.hold.reset' },
     'FM_AHR_STATUS' : { 'app' : 'open-ils.circ', 'method' : 'open-ils.circ.hold.status.retrieve' },
     'TRANSFER_TITLE_HOLDS' : { 'app' : 'open-ils.circ', 'method' : 'open-ils.circ.hold.change_title' },
+    'TRANSFER_SPECIFIC_TITLE_HOLDS' : { 'app' : 'open-ils.circ', 'method' : 'open-ils.circ.hold.change_title.specific_holds' },
     'FM_AHRCC_PCRUD_SEARCH' : { 'app' : 'open-ils.pcrud', 'method' : 'open-ils.pcrud.search.ahrcc.atomic', 'secure' : false },
     'FM_AIHU_CREATE' : { 'app' : 'open-ils.circ', 'method' : 'open-ils.circ.in_house_use.create' },
     'FM_ANCC_RETRIEVE_VIA_ID' : { 'app' : 'open-ils.circ', 'method' : 'open-ils.circ.non_cataloged_circulation.retrieve' },
index 7b3f53c..fc85a8f 100644 (file)
@@ -847,6 +847,10 @@ main.menu.prototype = {
                 ['oncommand'],
                 function(event) { open_eg_web_page('conify/global/config/coded_value_map', null, event); }
             ],
+            'cmd_server_admin_metabib_field' : [
+                ['oncommand'],
+                function(event) { open_eg_web_page('conify/global/config/metabib_field', null, event); }
+            ],
             'cmd_server_admin_acn_prefix' : [
                 ['oncommand'],
                 function(event) { open_eg_web_page('conify/global/config/acn_prefix', null, event); }
index 1f85b6e..8c79bbf 100644 (file)
              />
     <command id="cmd_server_admin_marc_code" />
     <command id="cmd_server_admin_coded_value_map" />
+    <command id="cmd_server_admin_metabib_field" />
     <command id="cmd_server_admin_billing_type" />
     <command id="cmd_server_admin_acn_prefix" />
     <command id="cmd_server_admin_acn_suffix" />
                 <menuitem label="&staff.main.menu.admin.server_admin.conify.acn_suffix.label;" command="cmd_server_admin_acn_suffix"/>
                 <menuitem label="&staff.main.menu.admin.server_admin.conify.marc_record_attrs.label;" command="cmd_server_admin_marc_code"/>
                 <menuitem label="&staff.main.menu.admin.server_admin.conify.coded_value_maps.label;" command="cmd_server_admin_coded_value_map"/>
+                <menuitem label="&staff.main.menu.admin.server_admin.conify.metabib_field.label;" command="cmd_server_admin_metabib_field"/>
                 <menuitem label="&staff.main.menu.admin.server_admin.conify.billing_type.label;" command="cmd_server_admin_billing_type"/>
                 <menuitem label="&staff.main.menu.admin.server_admin.conify.z3950_source.label;" command="cmd_server_admin_z39_source"/>
                 <menuitem label="&staff.main.menu.admin.server_admin.conify.circulation_modifier.label;" command="cmd_server_admin_circ_mod"/>
index 1edabec..a125c4d 100644 (file)
@@ -1716,6 +1716,7 @@ util.list.prototype = {
                 var col_id = prefix + hint + '_' + my_field.name;
                 var dataobj = hint;
                 var datafield = my_field.name;
+                var fleshed_display_field;
                 if (column_extras) {
                     if (column_extras['*']) {
                         if (column_extras['*']['dataobj']) {
@@ -1729,6 +1730,9 @@ util.list.prototype = {
                         if (column_extras[col_id]['datafield']) {
                             datafield = column_extras[col_id]['datafield'];
                         }
+                        if (column_extras[col_id]['fleshed_display_field']) {
+                            fleshed_display_field = column_extras[col_id]['fleshed_display_field'];
+                        }
                     }
                 }
                 var def = {
@@ -1743,7 +1747,24 @@ util.list.prototype = {
                 // my_field.datatype => bool float id int interval link money number org_unit text timestamp
                 if (my_field.datatype == 'link') {
                     def.render = function(my) { 
-                        return typeof my[dataobj][datafield]() == 'object' ? my[dataobj][datafield]()[my_field.key]() : my[dataobj][datafield](); 
+                        // is the object fleshed?
+                        return my[dataobj][datafield]() && typeof my[dataobj][datafield]() == 'object'
+                            // yes, show the display field
+                            ? my[dataobj][datafield]()[fleshed_display_field||my_field.key]()
+                            // no, do we have its class in data.hash?
+                            : ( typeof data.hash[ my[dataobj].Structure.field_map[datafield].class ] != 'undefined'
+                                // yes, do we have this particular object cached?
+                                ? ( data.hash[ my[dataobj].Structure.field_map[datafield].class ][ my[dataobj][datafield]() ]
+                                    // yes, show the display field
+                                    ? data.hash[ my[dataobj].Structure.field_map[datafield].class ][ my[dataobj][datafield]() ][
+                                        fleshed_display_field||my_field.key
+                                    ]()
+                                    // no, just show the raw value
+                                    : my[dataobj][datafield]()
+                                )
+                                // no, just show the raw value
+                                : my[dataobj][datafield]()
+                            ); 
                     }
                 } else {
                     def.render = function(my) { return my[dataobj][datafield](); }
index 7a734fc..9122655 100644 (file)
@@ -683,27 +683,42 @@ cat.copy_browser.prototype = {
                                             document.getElementById('commonStrings').getString('common.confirm')
                                     );
 
-                                    if (r == 0) {
+                                    if (r == 0) { // delete vols
                                         for (var i = 0; i < list.length; i++) {
                                             list[i].isdeleted('1');
                                         }
-                                        var robj = obj.network.simple_request(
-                                            'FM_ACN_TREE_UPDATE', 
-                                            [ ses(), list, true ],
-                                            null,
-                                            {
-                                                'title' : document.getElementById('catStrings').getString('staff.cat.copy_browser.delete_volume.override'),
-                                                'overridable_events' : [
-                                                ]
-                                            }
-                                        );
-                                        if (robj == null) throw(robj);
-                                        if (typeof robj.ilsevent != 'undefined') {
-                                            if (robj.ilsevent == 1206 /* VOLUME_NOT_EMPTY */) {
-                                                alert(document.getElementById('catStrings').getString('staff.cat.copy_browser.delete_volume.copies_remain'));
-                                                return;
+                                        var params = {};
+                                        loop: while(true) {
+                                            var robj = obj.network.simple_request(
+                                                'FM_ACN_TREE_UPDATE', 
+                                                [ ses(), list, true, params ],
+                                                null,
+                                                {
+                                                    'title' : document.getElementById('catStrings').getString('staff.cat.copy_browser.delete_volume.override'),
+                                                    'overridable_events' : [
+                                                    ]
+                                                }
+                                            );
+                                            if (robj == null) throw(robj);
+                                            if (typeof robj.ilsevent != 'undefined') {
+                                                if (robj.ilsevent == 1206 /* VOLUME_NOT_EMPTY */) {
+                                                    var r2 = obj.error.yns_alert(
+                                                        document.getElementById('catStrings').getString('staff.cat.copy_browser.delete_volume.copies_remain'),
+                                                        document.getElementById('catStrings').getString('staff.cat.copy_browser.delete_volume.title'),
+                                                        document.getElementById('catStrings').getString('staff.cat.copy_browser.delete_volume.copies_remain.confirm'),
+                                                        document.getElementById('catStrings').getString('staff.cat.copy_browser.delete_volume.copies_remain.cancel'),
+                                                        null,
+                                                        document.getElementById('commonStrings').getString('common.confirm')
+                                                    );
+                                                    if (r2 == 0) { // delete vols and copies
+                                                        params.force_delete_copies = true;
+                                                        continue loop;
+                                                    }
+                                                } else {
+                                                    if (robj.ilsevent != 0) throw(robj);
+                                                }
                                             }
-                                            if (robj.ilsevent != 0) throw(robj);
+                                            break loop;
                                         }
                                         obj.refresh_list();
                                     }
index 5512b36..3991283 100644 (file)
@@ -866,6 +866,12 @@ g.panes_and_field_names = {
         }
     ],
     [
+        $('catStrings').getString('staff.cat.copy_editor.field.active_date.label'),
+        { 
+            render: 'util.date.formatted_date( fm.active_date(), "%F");',
+        }
+    ],
+    [
         $('catStrings').getString('staff.cat.copy_editor.field.creator.label'),
         { 
             render: 'fm.creator();',
index 98e7e49..64358a4 100644 (file)
@@ -9,7 +9,9 @@ cat.util.EXPORT_OK    = [
     'spawn_copy_editor', 'add_copies_to_bucket', 'show_in_opac', 'spawn_spine_editor', 'transfer_copies', 
     'transfer_title_holds', 'mark_item_missing', 'mark_item_damaged', 'replace_barcode', 'fast_item_add', 
     'make_bookable', 'edit_new_brsrc', 'edit_new_bresv', 'batch_edit_volumes', 'render_fine_level',
-    'render_loan_duration', 'mark_item_as_missing_pieces'
+    'render_loan_duration', 'mark_item_as_missing_pieces', 'render_callnumbers_for_bib_menu',
+    'render_cn_prefix_menuitems', 'render_cn_suffix_menuitems', 'render_cn_class_menu',
+    'render_cn_prefix_menu', 'render_cn_suffix_menu', 'transfer_specific_title_holds'
 ];
 cat.util.EXPORT_TAGS    = { ':all' : cat.util.EXPORT_OK };
 
@@ -115,6 +117,37 @@ cat.util.transfer_title_holds = function(old_targets) {
     }
 }
 
+cat.util.transfer_specific_title_holds = function(hold_ids,unique_targets) {
+    JSAN.use('OpenILS.data'); var data = new OpenILS.data();
+    JSAN.use('util.network'); var network = new util.network();
+    try {
+        data.stash_retrieve();
+        var target = data.marked_record_for_hold_transfer;
+        if (!target) {
+            var m = $("catStrings").getString('staff.cat.opac.title_for_hold_transfer.destination_needed.label');
+            alert(m);
+            return;
+        }
+        if (unique_targets.length > 1) {
+            var m = $("catStrings").getString('staff.cat.opac.title_for_hold_transfer.many_bibs.warning');
+            if (! window.confirm(m)) {
+                return;
+            }
+        }
+        var robj = network.simple_request('TRANSFER_SPECIFIC_TITLE_HOLDS',[ ses(), target, hold_ids ]);
+        if (robj == 1) {
+            var m = $("catStrings").getString('staff.cat.opac.title_for_hold_transfer.success.label');
+            alert(m);
+        } else {
+            var m = $("catStrings").getString('staff.cat.opac.title_for_hold_transfer.failure.label');
+            alert(m);
+        }
+    } catch(E) {
+        alert('Error in cat.util.transfer_title.holds(): ' + E);
+    }
+}
+
+
 cat.util.transfer_copies = function(params) {
     JSAN.use('util.error'); var error = new util.error();
     JSAN.use('OpenILS.data'); var data = new OpenILS.data();
@@ -887,4 +920,202 @@ cat.util.mark_item_as_missing_pieces = function(copy_ids) {
     }
 }
 
+cat.util.render_callnumbers_for_bib_menu = function(node, doc_id, label_class) {
+    try {
+        var cn_blob;
+        try {
+            cn_blob = g.network.simple_request('BLOB_MARC_CALLNUMBERS_RETRIEVE',[doc_id, label_class]);
+        } catch(E) {
+            cn_blob = [];
+        }
+        var hbox = typeof node == 'string' ? document.getElementById(node) : node;
+        JSAN.use('util.widgets');
+        JSAN.use('util.functional');
+        var ml = util.widgets.make_menulist(
+            [
+                [ '', '' ]
+            ].concat(
+                util.functional.map_list(
+                    cn_blob,
+                    function(o) {
+                        for (var i in o) {
+                            return [ o[i], i ];
+                        }
+                    }
+                )
+            )
+        ); hbox.appendChild(ml);
+        ml.setAttribute('editable','true');
+        ml.setAttribute('width', '200');
+        ml.setAttribute('id', hbox.id + '_menulist');
+    } catch(E) {
+        alert('Error in cat.util.render_callnumbers_for_bib_menu: ' + E);
+    }
+}
+
+cat.util.render_cn_prefix_menuitems = function(menupopup,ou_id) {
+    try {
+        JSAN.use('OpenILS.data');
+        var data = new OpenILS.data(); data.stash_retrieve();
+        JSAN.use('util.network');
+        var network = new util.network();
+
+        if (typeof data.list['acnp_for_lib_'+ou_id] == 'undefined') {
+            data.list['acnp_for_lib_'+ou_id] = network.simple_request(
+                'FM_ACNP_RETRIEVE_VIA_PCRUD',
+                [ ses(), {"owning_lib":{"=":ou_id}}, {"order_by":{"acnp":"label_sortkey"}} ]
+            );
+            data.stash('list');
+        }
+        for (var i = 0; i < data.list['acnp_for_lib_'+ou_id].length; i++) {
+            var my_acnp = data.list['acnp_for_lib_'+ou_id][i];
+            var menuitem = document.createElement('menuitem');
+            menupopup.appendChild(menuitem);
+                menuitem.setAttribute(
+                    'label',
+                    my_acnp.id() == -1 ? '' :
+                    $('catStrings').getFormattedString(
+                        'staff.cat.volume_copy_creator.call_number_prefix.menuitem_label',
+                        [
+                            my_acnp.label(),
+                            data.hash.aou[ ou_id ].shortname()
+                        ]
+                    )
+                );
+                menuitem.setAttribute('value',my_acnp.id());
+        }
+    } catch(E) {
+        alert('Error in cat.util.render_cn_prefix_menuitems: ' + E);
+    }
+}
+
+cat.util.render_cn_suffix_menuitems = function(menupopup,ou_id) {
+    try {
+        JSAN.use('OpenILS.data');
+        var data = new OpenILS.data(); data.stash_retrieve();
+        JSAN.use('util.network');
+        var network = new util.network();
+
+        if (typeof data.list['acns_for_lib_'+ou_id] == 'undefined') {
+            data.list['acns_for_lib_'+ou_id] = network.simple_request(
+                'FM_ACNS_RETRIEVE_VIA_PCRUD',
+                [ ses(), {"owning_lib":{"=":ou_id}}, {"order_by":{"acns":"label_sortkey"}} ]
+            );
+            data.stash('list');
+        }
+        for (var i = 0; i < data.list['acns_for_lib_'+ou_id].length; i++) {
+            var my_acns = data.list['acns_for_lib_'+ou_id][i];
+            var menuitem = document.createElement('menuitem');
+            menupopup.appendChild(menuitem);
+                menuitem.setAttribute(
+                    'label',
+                    my_acns.id() == -1 ? '' :
+                    $('catStrings').getFormattedString(
+                        'staff.cat.volume_copy_creator.call_number_suffix.menuitem_label',
+                        [
+                            my_acns.label(),
+                            data.hash.aou[ ou_id ].shortname()
+                        ]
+                    )
+                );
+                menuitem.setAttribute('value',my_acns.id());
+        }
+    } catch(E) {
+        alert('Error in cat.util.render_cn_suffix_menuitems: ' + E);
+    }
+}
+
+cat.util.render_cn_class_menu = function(extra_menuitems,menu_default) {
+    try {
+        JSAN.use('util.widgets');
+        JSAN.use('OpenILS.data');
+        var data = new OpenILS.data(); data.stash_retrieve();
+
+        var menulist = util.widgets.make_menulist(
+            (extra_menuitems || []).concat(
+                util.functional.map_list(
+                    data.list.acnc,
+                    function(o) {
+                        return [ o.name(), o.id() ];
+                    }
+                )
+            )
+        );
+
+        if (typeof menu_default != 'undefined') {
+            menulist.setAttribute('value',menu_default);
+        }
+        return menulist;
+
+    } catch(E) {
+        alert('Error in cat.util.render_cn_class_menu: ' + E);
+    }
+}
+
+cat.util.render_cn_prefix_menu = function(ou_ids,extra_menuitems,menu_default) {
+    try {
+        JSAN.use('util.widgets');
+        var menulist = util.widgets.make_menulist(extra_menuitems||[],menu_default);
+            var menupopup = menulist.firstChild;
+            var org_list;
+            if (ou_ids.length == 1) {
+                JSAN.use('OpenILS.data');
+                var data = new OpenILS.data(); data.stash_retrieve();
+                var org = data.hash.aou[ ou_ids[0] ];
+                org_list = []; // order from top of consortium to owning lib
+                while(org) {
+                    org_list.unshift(org.id());
+                    org = org.parent_ou();
+                    if (org && typeof org != 'object') {
+                        org = data.hash.aou[ org ];
+                    }
+                }
+            } else {
+                org_list = ou_ids;
+            }
+            for (var i = 0; i < org_list.length; i++) {
+                cat.util.render_cn_prefix_menuitems(menupopup,org_list[i]);
+            }
+        if (typeof menu_default != 'undefined') {
+            menulist.setAttribute('value',menu_default);
+        }
+        return menulist;
+    } catch(E) {
+        alert('Error in cat.util.render_cn_prefix_menu('+ou_id+'): ' + E);
+    }
+}
+
+cat.util.render_cn_suffix_menu = function(ou_ids,extra_menuitems,menu_default) {
+    try {
+        JSAN.use('util.widgets');
+        var menulist = util.widgets.make_menulist(extra_menuitems||[],menu_default);
+            var menupopup = menulist.firstChild;
+            var org_list;
+            if (ou_ids.length == 1) {
+                JSAN.use('OpenILS.data');
+                var data = new OpenILS.data(); data.stash_retrieve();
+                var org = data.hash.aou[ ou_ids[0] ];
+                org_list = []; // order from top of consortium to owning lib
+                while(org) {
+                    org_list.unshift(org.id());
+                    org = org.parent_ou();
+                    if (org && typeof org != 'object') {
+                        org = data.hash.aou[ org ];
+                 &nbs