Merge branch 'master' of ssh://yeti.esilibrary.com/home/evergreen/evergreen-equinox...
authorsenator <lebbeous@esilibrary.com>
Wed, 23 Mar 2011 14:32:49 +0000 (10:32 -0400)
committersenator <lebbeous@esilibrary.com>
Wed, 23 Mar 2011 14:32:49 +0000 (10:32 -0400)
28 files changed:
Open-ILS/examples/apache/eg.conf
Open-ILS/examples/crontab.example
Open-ILS/examples/fm_IDL.xml
Open-ILS/src/extras/Makefile.install
Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/action.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/config.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Driver/Pg/QueryParser.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/action.pm
Open-ILS/src/python/oils/const.py
Open-ILS/src/python/oils/system.py
Open-ILS/src/python/oils/utils/csedit.py
Open-ILS/src/python/oils/utils/idl.py
Open-ILS/src/python/oils/utils/utils.py
Open-ILS/src/sql/Pg/002.schema.config.sql
Open-ILS/src/sql/Pg/090.schema.action.sql
Open-ILS/src/sql/Pg/100.circ_matrix.sql
Open-ILS/src/sql/Pg/950.data.seed-values.sql
Open-ILS/src/sql/Pg/upgrade/0503.schema.grace_periods.sql [new file with mode: 0644]
Open-ILS/src/support-scripts/fine_generator.pl
Open-ILS/web/js/ui/default/conify/global/config/circ_matrix_matchpoint.js
Open-ILS/web/opac/common/js/opac_utils.js
Open-ILS/web/opac/skin/default/xml/rdetail/rdetail_summary.xml
Open-ILS/web/templates/default/conify/global/config/circ_matrix_matchpoint.tt2
Open-ILS/web/templates/default/conify/global/config/rule_recurring_fine.tt2
Open-ILS/xul/staff_client/Makefile.am
Open-ILS/xul/staff_client/windowssetup.nsi
build/tools/update_db.sh

index 537baa4..56ddea9 100644 (file)
@@ -93,11 +93,11 @@ Alias /updates/ "/openils/var/updates/pub/"
 # OPTIONAL: Set how long the client will cache our content.  Change to suit
 # ----------------------------------------------------------------------------------
 ExpiresActive On
-ExpiresDefault A2592000
-ExpiresByType text/html A64800
-ExpiresByType application/xhtml+xml A64800
-ExpiresByType application/x-javascript A64800
-ExpiresByType text/css A3000
+ExpiresDefault "access plus 1 month"
+ExpiresByType text/html "access plus 18 hours"
+ExpiresByType application/xhtml+xml "access plus 18 hours"
+ExpiresByType application/x-javascript "access plus 18 hours"
+ExpiresByType text/css "access plus 50 minutes"
 
 # ----------------------------------------------------------------------------------
 # Set up our SSL virtual host
index 2f39344..1cd1db3 100644 (file)
@@ -31,7 +31,7 @@ EG_BIN_DIR = /openils/bin
 # m h dom mon dow   command
 
 # Run the hold targeter
-* */4 * * *   . ~/.bashrc && $EG_BIN_DIR/hold_targeter.pl $SRF_CORE
+*/15 * * * *   . ~/.bashrc && $EG_BIN_DIR/hold_targeter.pl $SRF_CORE
 
 # Run the hold thawer
 5  0  * * *   . ~/.bashrc && $EG_BIN_DIR/thaw_expired_frozen_holds.srfsh
@@ -61,7 +61,7 @@ EG_BIN_DIR = /openils/bin
 # Action/Trigger entries ----
 
 # Runs all pending A/T events every half hour
-0 */2 * * * . ~/.bashrc && $EG_BIN_DIR/action_trigger_runner.pl --osrf-config $SRF_CORE --run-pending
+*/30 * * * * . ~/.bashrc && $EG_BIN_DIR/action_trigger_runner.pl --osrf-config $SRF_CORE --run-pending
 
 # Passive A/T event generation.
 # Note: push these back to 3am so they will run after the fine generator and spread out the start minute to reduce dogpiling
index 6d3cf94..8836291 100644 (file)
@@ -1245,6 +1245,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <field reporter:label="Max Fine Rule" name="max_fine_rule" reporter:datatype="link"/>
             <field reporter:label="Hard Due Date" name="hard_due_date" reporter:datatype="link"/>
             <field reporter:label="Renewals Override" name="renewals" reporter:datatype="int"/>
+            <field reporter:label="Grace Period Override" name="grace_period" reporter:datatype="interval"/>
                        <field reporter:label="Script Test" name="script_test" reporter:datatype="text"/>
                        <field name="total_copy_hold_ratio" reporter:datatype="float" reporter:label="Minimum Total Copy/Hold Ratio"/>
                        <field name="available_copy_hold_ratio" reporter:datatype="float" reporter:label="Minimum Available Copy/Hold Ratio"/>
@@ -2805,6 +2806,7 @@ SELECT  usr,
                        <field reporter:label="Recurring Fine Amount" name="recurring_fine" reporter:datatype="money" />
                        <field reporter:label="Recurring Fine Rule" name="recurring_fine_rule" reporter:datatype="link"/>
                        <field reporter:label="Remaining Renewals" name="renewal_remaining" reporter:datatype="int" />
+                       <field reporter:label="Grace Period" name="grace_period" reporter:datatype="interval" />
                        <field reporter:label="Fine Stop Reason" name="stop_fines" reporter:datatype="text"/>
                        <field reporter:label="Fine Stop Date/Time" name="stop_fines_time" reporter:datatype="timestamp"/>
                        <field reporter:label="Circulating Item" name="target_copy" reporter:datatype="link"/>
@@ -2866,6 +2868,7 @@ SELECT  usr,
                        <field reporter:label="Recurring Fine Amount" name="recurring_fine" reporter:datatype="money" />
                        <field reporter:label="Recurring Fine Rule" name="recurring_fine_rule" reporter:datatype="link"/>
                        <field reporter:label="Remaining Renewals" name="renewal_remaining" reporter:datatype="int" />
+                       <field reporter:label="Grace Period" name="grace_period" reporter:datatype="interval" />
                        <field reporter:label="Fine Stop Reason" name="stop_fines" reporter:datatype="text"/>
                        <field reporter:label="Fine Stop Date/Time" name="stop_fines_time" reporter:datatype="timestamp"/>
                        <field reporter:label="Circulating Item" name="target_copy" reporter:datatype="link"/>
@@ -2930,6 +2933,7 @@ SELECT  usr,
                        <field reporter:label="Recurring Fine Amount" name="recurring_fine" reporter:datatype="money" />
                        <field reporter:label="Recurring Fine Rule" name="recurring_fine_rule" reporter:datatype="link"/>
                        <field reporter:label="Remaining Renewals" name="renewal_remaining" reporter:datatype="int" />
+                       <field reporter:label="Grace Period" name="grace_period" reporter:datatype="interval" />
                        <field reporter:label="Fine Stop Reason" name="stop_fines" reporter:datatype="text"/>
                        <field reporter:label="Fine Stop Date/Time" name="stop_fines_time" reporter:datatype="timestamp"/>
                        <field reporter:label="Circulating Item" name="target_copy" reporter:datatype="link"/>
@@ -4571,6 +4575,7 @@ SELECT  usr,
                        <field name="recurring_fine" reporter:datatype="money" />
                        <field name="recurring_fine_rule" reporter:datatype="link"/>
                        <field name="renewal_remaining" reporter:datatype="int" />
+            <field name="grace_period" reporter:datatype="interval" />
                        <field name="stop_fines" reporter:datatype="text"/>
                        <field name="stop_fines_time" reporter:datatype="timestamp"/>
                        <field name="target_copy" reporter:datatype="link"/>
@@ -4606,6 +4611,7 @@ SELECT  usr,
                        <field name="name" reporter:datatype="text"/>
                        <field name="normal" reporter:datatype="money" />
                        <field name="recurrence_interval" reporter:datatype="interval"/>
+            <field name="grace_period" reporter:datatype="interval" />
                </fields>
                <links/>
         <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
index fef69ef..2068d9f 100644 (file)
@@ -157,7 +157,6 @@ CENTOS_PERL = \
 FEDORA_13_RPMS = \
        aspell \
        aspell-en \
-       js-devel \
        libdbi \
        libdbi-dbd-pgsql \
        libdbi-devel \
@@ -270,20 +269,20 @@ centos: install_centos_pgsql centos_like
 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
 
-fedora13: install_fedora_13_rpms install_cpan install_cpan_fedora install_cpan_marc install_spidermonkey install_cpan_force
+fedora13: install_fedora_13_rpms install_cpan install_cpan_fedora install_cpan_marc install_js_sm install_cpan_force
 fedora14: fedora13
 
 debian-lenny: lenny generic_debian install_cpan_more install_cpan_safe
 debian-squeeze: squeeze generic_debian
 lenny: install_pgsql_client_debs_83 install_extra_debs
 squeeze: install_pgsql_client_debs_84  install_extra_debs_squeeze
-generic_debian:  install_debs install debian_sys_config install_cpan_force
+generic_debian:  install_debs test_for_libdbi_pkg install debian_sys_config install_cpan_force
 
 ubuntu-hardy: hardy generic_ubuntu
 ubuntu-lucid: lucid generic_ubuntu
 hardy: install_pgsql_client_debs_82 install_yaz install_cpan_marc install_extra_encode
 lucid: install_pgsql_client_debs_84 install_extra_debs
-generic_ubuntu: install_debs install debian_sys_config install_cpan_more install_cpan_safe install_cpan_force
+generic_ubuntu: install_debs test_for_libdbi_pkg install debian_sys_config install_cpan_more install_cpan_safe install_cpan_force
 
 # - COMMON TARGETS ---------------------------------------------------------
 
@@ -326,7 +325,7 @@ install_js_sm: install_libjs install_spidermonkey
 install_libjs: 
        if [ ! -f $(LIBJS).tar.gz ]; then wget $(LIBJS_URL); fi;
        tar -zxf $(LIBJS).tar.gz
-       cd js/src/ && JS_THREADSAFE=true JS_DIST=/usr make -f Makefile.ref
+       cd js/src/ && JS_DIST=/usr make -f Makefile.ref
        mkdir -p $(JS_INSTALL_PREFIX)/include/js/
        cp js/src/*.h $(JS_INSTALL_PREFIX)/include/js/
        cp js/src/*.tbl $(JS_INSTALL_PREFIX)/include/js/
@@ -342,8 +341,22 @@ install_spidermonkey:
        if [ ! -z $(FEDORA) ]; then \
                sed -i -e 's/js32.dll/libjs.so/' $(LIBJS_PERL)/Makefile.PL ; \
        fi;
-       cd $(LIBJS_PERL) && perl Makefile.PL -E4X -JS_THREADSAFE && make && make test && make install
-
+       cd $(LIBJS_PERL) && perl Makefile.PL -E4X && make && make test && make install
+
+# On Ubuntu and possibly Debian, the libdbi0 package prevents the 
+# compiled-from-source version from being used and breaks the install.
+# This package might get installed depending on the install-time choices
+# for the distro. Test for its existence; if it's there, throw an error
+# message and exit.
+test_for_libdbi_pkg:
+               @if [ "$$(apt-cache policy libdbi0 | grep Installed | grep none | wc -l)" -eq 0 ]; then \
+                               echo "*** Detected locally installed libdbi0 package; you must remove this"; \
+                               echo "*** with a command like 'aptitude remove libdbi0' before proceeding"; \
+                               echo "*** to successfully install Evergreen."; \
+                               echo; \
+                               echo "*** Note: this may break other applications on your system."; \
+                               exit 0; \
+               fi;
 
 # Install libdbi and the postgres drivers
 install_libdbi:
index 0e46529..10e9a34 100644 (file)
@@ -1162,6 +1162,9 @@ sub run_indb_circ_test {
             $self->circ_matrix_matchpoint->duration_rule->max_renewals($results->[0]->{renewals});
         }
         $self->circ_matrix_matchpoint->recurring_fine_rule($self->editor->retrieve_config_rules_recurring_fine($results->[0]->{recurring_fine_rule}));
+        if($results->[0]->{grace_period}) {
+            $self->circ_matrix_matchpoint->recurring_fine_rule->grace_period($results->[0]->{grace_period});
+        }
         $self->circ_matrix_matchpoint->max_fine_rule($self->editor->retrieve_config_rules_max_fine($results->[0]->{max_fine_rule}));
         $self->circ_matrix_matchpoint->hard_due_date($self->editor->retrieve_config_hard_due_date($results->[0]->{hard_due_date}));
     }
@@ -1220,7 +1223,8 @@ sub get_circ_policy {
         max_fine_rule => $max_fine_rule->name,
         max_fine => $self->get_max_fine_amount($max_fine_rule),
         fine_interval => $recurring_fine_rule->recurrence_interval,
-        renewal_remaining => $duration_rule->max_renewals
+        renewal_remaining => $duration_rule->max_renewals,
+        grace_period => $recurring_fine_rule->grace_period
     };
 
     if($hard_due_date) {
@@ -1833,6 +1837,7 @@ sub build_checkout_circ_object {
         $circ->max_fine($policy->{max_fine});
         $circ->fine_interval($recurring->recurrence_interval);
         $circ->renewal_remaining($duration->max_renewals);
+        $circ->grace_period($policy->{grace_period});
 
     } else {
 
@@ -1841,6 +1846,7 @@ sub build_checkout_circ_object {
         $circ->recurring_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
         $circ->max_fine_rule(OILS_UNLIMITED_CIRC_DURATION);
         $circ->renewal_remaining(0);
+        $circ->grace_period(0);
     }
 
    $circ->target_copy( $copy->id );
@@ -2928,15 +2934,25 @@ sub generate_fines {
 sub generate_fines_start {
    my $self = shift;
    my $reservation = shift;
-
-   my $id = $reservation ? $self->reservation->id : $self->circ->id;
+   my $dt_parser = DateTime::Format::ISO8601->new;
+
+   my $obj = $reservation ? $self->reservation : $self->circ;
+
+   # If we have a grace period
+   if($obj->can('grace_period')) {
+      # Parse out the due date
+      my $due_date = $dt_parser->parse_datetime( cleanse_ISO8601($obj->due_date) );
+      # Add the grace period to the due date
+      $due_date->add(seconds => OpenSRF::Utils->interval_to_seconds($obj->grace_period));
+      # Don't generate fines on circs still in grace period
+      return undef if ($due_date > DateTime->now);
+   }
 
    if (!exists($self->{_gen_fines_req})) {
       $self->{_gen_fines_req} = OpenSRF::AppSession->create('open-ils.storage') 
           ->request(
              'open-ils.storage.action.circulation.overdue.generate_fines',
-             undef,
-             $id
+             $obj->id
           );
    }
 
@@ -2947,6 +2963,8 @@ sub generate_fines_finish {
    my $self = shift;
    my $reservation = shift;
 
+   return undef unless $self->{_gen_fines_req};
+
    my $id = $reservation ? $self->reservation->id : $self->circ->id;
 
    $self->{_gen_fines_req}->wait_complete;
index 99772dc..06094b4 100644 (file)
@@ -63,7 +63,7 @@ use base qw/action/;
 __PACKAGE__->table('action_circulation');
 __PACKAGE__->columns(Primary => 'id');
 __PACKAGE__->columns(Essential => qw/xact_start usr target_copy circ_lib
-                                    duration duration_rule renewal_remaining
+                                    duration duration_rule renewal_remaining grace_period
                                     recurring_fine_rule recurring_fine stop_fines
                                     max_fine max_fine_rule fine_interval
                                     stop_fines xact_finish due_date opac_renewal
@@ -78,7 +78,7 @@ use base qw/action/;
 __PACKAGE__->table('action_open_circulation');
 __PACKAGE__->columns(Primary => 'id');
 __PACKAGE__->columns(Essential => qw/xact_start usr target_copy circ_lib
-                                    duration duration_rule renewal_remaining
+                                    duration duration_rule renewal_remaining grace_period
                                     recurring_fine_rule recurring_fine stop_fines
                                     max_fine max_fine_rule fine_interval
                                     stop_fines xact_finish due_date opac_renewal
index bde4372..da03f1f 100644 (file)
@@ -59,7 +59,7 @@ package config::rules::recurring_fine;
 use base qw/config/;
 __PACKAGE__->table('config_rule_recurring_fine');
 __PACKAGE__->columns(Primary => 'id');
-__PACKAGE__->columns(Essential => qw/name high normal low recurrence_interval/);
+__PACKAGE__->columns(Essential => qw/name high normal low recurrence_interval grace_period/);
 #-------------------------------------------------------------------------------
 
 package config::rules::age_hold_protect;
index a930931..c74ffee 100644 (file)
@@ -208,7 +208,11 @@ sub add_relevance_bump {
     my $multiplier = shift;
     my $active = shift;
 
-    $active = 1 if (!defined($active));
+    if (defined($active) and $active eq 'f') {
+        $active = 0;
+    } else {
+        $active = 1;
+    }
 
     $self->relevance_bumps->{$class}{$field}{$type} = { multiplier => $multiplier, active => $active };
 
@@ -248,7 +252,7 @@ sub initialize_relevance_bumps {
 
     for my $sra (@$sra_list) {
         my $c = $self->search_field_class_by_id( $sra->field );
-        __PACKAGE__->add_relevance_bump( $c->{classname}, $c->{field}, $sra->bump_type, $sra->multiplier );
+        __PACKAGE__->add_relevance_bump( $c->{classname}, $c->{field}, $sra->bump_type, $sra->multiplier, $sra->active );
     }
 
     return $self->relevance_bumps;
index e8aee2f..0760fcb 100644 (file)
@@ -95,23 +95,16 @@ __PACKAGE__->register_method(
 
 
 sub overdue_circs {
-       my $grace = shift;
     my $upper_interval = shift || '1 millennium';
        my $idlist = shift;
 
        my $c_t = action::circulation->table;
 
-       if ($grace && $grace =~ /^\d+$/o) {
-       $grace = " - ($grace * (fine_interval))";
-    } else {
-        $grace = '';
-    } 
-
        my $sql = <<"   SQL";
                SELECT  *
                  FROM  $c_t
                  WHERE stop_fines IS NULL
-                       AND due_date < ( CURRENT_TIMESTAMP $grace)
+                       AND due_date < ( CURRENT_TIMESTAMP - grace_period )
             AND fine_interval < ?::INTERVAL
        SQL
 
@@ -125,7 +118,7 @@ sub overdue_circs {
                SELECT  *
                  FROM  $c_t
                  WHERE return_time IS NULL
-                       AND end_time < ( CURRENT_TIMESTAMP $grace)
+                       AND end_time < ( CURRENT_TIMESTAMP )
             AND fine_interval IS NOT NULL
             AND cancel_time IS NULL
        SQL
@@ -266,11 +259,10 @@ __PACKAGE__->register_method(
 sub grab_overdue {
        my $self = shift;
        my $client = shift;
-       my $grace = shift || '';
 
        my $idlist = $self->api_name =~/id_list/o ? 1 : 0;
     
-       $client->respond( $idlist ? $_ : $_->to_fieldmapper ) for ( overdue_circs($grace, '', $idlist) );
+       $client->respond( $idlist ? $_ : $_->to_fieldmapper ) for ( overdue_circs('', $idlist) );
 
        return undef;
 
@@ -787,7 +779,6 @@ sub seconds_to_interval_hash {
 sub generate_fines {
        my $self = shift;
        my $client = shift;
-       my $grace = shift;
        my $circ = shift;
        my $overbill = shift;
 
@@ -799,7 +790,7 @@ sub generate_fines {
             action::circulation->search_where( { id => $circ, stop_fines => undef } ),
             booking::reservation->search_where( { id => $circ, return_time => undef, cancel_time => undef } );
        } else {
-               push @circs, overdue_circs($grace);
+               push @circs, overdue_circs();
        }
 
        my %hoo = map { ( $_->id => $_ ) } actor::org_unit::hours_of_operation->retrieve_all;
@@ -823,6 +814,8 @@ sub generate_fines {
             $recurring_fine_method = 'fine_amount';
             next unless ($c->fine_interval);
         }
+        #TODO: reservation grace periods
+        my $grace_period = ($is_reservation ? 0 : $c->grace_period);
 
                try {
                        if ($self->method_lookup('open-ils.storage.transaction.current')->run) {
@@ -902,8 +895,8 @@ sub generate_fines {
                                                while ( $h->$dow_open eq '00:00:00' and $h->$dow_close eq '00:00:00' ) {
                                                        # if the circ lib is closed, add a day to the grace period...
 
-                                                       $grace++;
-                                                       $log->info( "Grace period for circ ".$c->id." extended to $grace intervals" );
+                                                       $grace_period+=86400;
+                                                       $log->info( "Grace period for circ ".$c->id." extended to $grace_period [" . seconds_to_interval( $grace_period ) . "]" );
                                                        $log->info( "Day of week $dow open $dow_open, close $dow_close" );
 
                                                        $due_dt = $due_dt->add( days => 1 );
@@ -927,12 +920,11 @@ sub generate_fines {
             $pending_fine_count++ if ($fine_interval && ($fine_interval % 86400 == 0));
 
             if ( $last_fine == $due                         # we have no fines yet
-                 && $grace                                  # and we have a grace period
-                 && $pending_fine_count <= $grace           # and we seem to be inside that period
-                 && $now < $due + $fine_interval * $grace   # and some date math bares that out, then
+                 && $grace_period                           # and we have a grace period
+                 && $now < $due + $grace_period             # and some date math says were are within the grace period
             ) {
-                $client->respond( "Still inside grace period of: ". seconds_to_interval( $fine_interval * $grace)."\n" );
-                $log->info( "Circ ".$c->id." is still inside grace period of: $grace [". seconds_to_interval( $fine_interval * $grace).']' );
+                $client->respond( "Still inside grace period of: ". seconds_to_interval( $grace_period )."\n" );
+                $log->info( "Circ ".$c->id." is still inside grace period of: $grace_period [". seconds_to_interval( $grace_period ).']' );
                 next;
             }
 
index a7bfc38..2c4d240 100644 (file)
@@ -1,3 +1,8 @@
+"""
+Defines Evergreen constants, including namespaces, events, and services
+
+The OILS prefix derives from Evergreen's old working title, Open-ILS.
+"""
 # -----------------------------------------------------------------------
 # Copyright (C) 2007  Georgia Public Library Service
 # Bill Erickson <erickson@esilibrary.com>
 # GNU General Public License for more details.
 # -----------------------------------------------------------------------
 
-
-OILS_NS_OBJ='http://open-ils.org/spec/opensrf/IDL/objects/v1'
-OILS_NS_PERSIST='http://open-ils.org/spec/opensrf/IDL/persistence/v1'
-OILS_NS_REPORTER='http://open-ils.org/spec/opensrf/IDL/reporter/v1'
-
+OILS_NS_OBJ = 'http://open-ils.org/spec/opensrf/IDL/objects/v1'
+OILS_NS_PERSIST = 'http://open-ils.org/spec/opensrf/IDL/persistence/v1'
+OILS_NS_REPORTER = 'http://open-ils.org/spec/opensrf/IDL/reporter/v1'
 
 OILS_EVENT_SUCCESS = 'SUCCESS'
 
 OILS_APP_AUTH = 'open-ils.auth'
 OILS_APP_CIRC = 'open-ils.circ'
-OILS_APP_CSTORE='open-ils.cstore'
-OILS_APP_SEARCH='open-ils.search'
-OILS_APP_ACQ='open-ils.acq'
-OILS_APP_ACTOR='open-ils.actor'
+OILS_APP_CSTORE = 'open-ils.cstore'
+OILS_APP_SEARCH = 'open-ils.search'
+OILS_APP_ACQ = 'open-ils.acq'
+OILS_APP_ACTOR = 'open-ils.actor'
index 17ae11d..132df7c 100644 (file)
@@ -19,20 +19,26 @@ from oils.utils.idl import IDLParser
 from oils.utils.csedit import oilsLoadCSEditor
 
 class System(object):
+
     @staticmethod
     def connect(**kwargs):
-           """Connects to the opensrf network,  parses the IDL file, and loads the CSEditor"""
-           osrf.system.System.connect(**kwargs)
-           IDLParser.parse()
-           oilsLoadCSEditor()
+        """
+        Connects to the OpenSRF network, parses the IDL, and loads the CSEditor.
+        """
+
+        osrf.system.System.connect(**kwargs)
+        IDLParser.parse()
+        oilsLoadCSEditor()
 
     @staticmethod
     def remote_connect(**kwargs):
-           """
-            Connects to the opensrf network,  parses the IDL file, and loads the CSEditor.
-            This version of connect does not talk to opensrf.settings, which means it 
-            also does not connect to the opensrf cache.
         """
-           osrf.system.System.net_connect(**kwargs)
-           IDLParser.parse()
-           oilsLoadCSEditor()
+        Connects to the OpenSRF network, parses the IDL, and loads the CSEditor.
+
+        This version of connect does not talk to opensrf.settings, which means
+        it also does not connect to the OpenSRF cache.
+        """
+
+        osrf.system.System.net_connect(**kwargs)
+        IDLParser.parse()
+        oilsLoadCSEditor()
index d21f297..9877642 100644 (file)
@@ -1,3 +1,6 @@
+"""
+A Python-friendly wrapper for accessing the Evergreen open-ils.cstore service
+"""
 # -----------------------------------------------------------------------
 # Copyright (C) 2007  Georgia Public Library Service
 # Bill Erickson <billserickson@gmail.com>
 # GNU General Public License for more details.
 # -----------------------------------------------------------------------
 
-from osrf.log import *
-from osrf.json import *
 from oils.utils.idl import IDLParser
+from osrf.const import OSRF_APP_SESSION_CONNECTED
+from osrf.log import log_debug, log_info, log_error
 from osrf.ses import ClientSession
-from oils.const import *
+import oils.const
 import re
 
 ACTIONS = ['create', 'retrieve', 'batch_retrieve', 'update', 'delete', 'search']
@@ -87,7 +90,7 @@ class CSEditor(object):
                 connect time.  xact implies connect.
         '''
 
-        self.app = args.get('app', OILS_APP_CSTORE)
+        self.app = args.get('app', oils.const.OILS_APP_CSTORE)
         self.authtoken = args.get('authtoken', args.get('auth'))
         self.requestor = args.get('requestor')
         self.connect = args.get('connect')
@@ -106,15 +109,17 @@ class CSEditor(object):
         '''
         pass
 
-
     # -------------------------------------------------------------------------
     # Creates a session if one does not already exist.  If necessary, connects
     # to the remote service and starts a transaction
     # -------------------------------------------------------------------------
     def session(self, ses=None):
-        ''' Creates a session if one does not already exist.  If necessary, connects
-            to the remote service and starts a transaction
-        '''
+        """
+        Creates a session if one does not already exist.
+
+        If necessary, connects to the remote service and starts a transaction.
+        """
+
         if not self.__session:
             self.__session = ClientSession(self.app)
 
@@ -127,27 +132,30 @@ class CSEditor(object):
             self.request(self.app + '.transaction.begin')
 
         return self.__session
-   
 
     def log(self, func, string):
         ''' Logs string with some meta info '''
 
-        s = "editor[";
-        if self.xact: s += "1|"
-        else: s += "0|"
-        if self.requestor: s += str(self.requestor.id())
-        else: s += "0"
-        s += "]"
-        func("%s %s" % (s, string))
+        meta = "editor["
+        if self.xact:
+            meta += "1|"
+        else:
+            meta += "0|"
 
+        if self.requestor:
+            meta += str(self.requestor.id())
+        else:
+            meta += "0"
+        meta += "]"
+        func("%s %s" % (meta, string))
 
     def rollback(self):
         ''' Rolls back the existing db transaction '''
 
         if self.__session and self.xact:
-             self.log(log_info, "rolling back db transaction")
-             self.request(self.app + '.transaction.rollback')
-             self.disconnect()
+            self.log(log_info, "rolling back db transaction")
+            self.request(self.app + '.transaction.rollback')
+            self.disconnect()
              
     def commit(self):
         ''' Commits the existing db transaction and disconnects '''
@@ -157,18 +165,16 @@ class CSEditor(object):
             self.request(self.app + '.transaction.commit')
             self.disconnect()
 
-
     def disconnect(self):
         ''' Disconnects from the remote service '''
         if self.__session:
             self.__session.disconnect()
             self.__session = None
 
-
-    # -------------------------------------------------------------------------
-    # Sends a request
-    # -------------------------------------------------------------------------
     def request(self, method, params=[]):
+        """
+        Sends a request.
+        """
 
         # XXX improve param logging here
 
@@ -190,7 +196,6 @@ class CSEditor(object):
 
         return val
 
-
     # -------------------------------------------------------------------------
     # Returns true if our requestor is allowed to perform the request action
     # 'org' defaults to the requestors ws_ou
@@ -198,10 +203,9 @@ class CSEditor(object):
     def allowed(self, perm, org=None):
         pass # XXX
 
+    def runMethod(self, action, obj_type, arg, options={}):
 
-    def runMethod(self, action, type, arg, options={}):
-
-        method = "%s.direct.%s.%s" % (self.app, type, action)
+        method = "%s.direct.%s.%s" % (self.app, obj_type, action)
 
         if options.get('idlist'):
             method = method.replace('search', 'id_list')
@@ -215,7 +219,7 @@ class CSEditor(object):
             method += '.atomic'
             arg = {'id' : arg}
 
-        params = [arg];
+        params = [arg]
         if len(options.keys()):
             params.append(options)
 
@@ -239,34 +243,33 @@ class CSEditor(object):
         }
         return self.rawSearch(args)
 
-
     def fieldSearch(self, hint, fields, where):
         return self.rawSearch2(hint, fields, where)
 
-
-
-# -------------------------------------------------------------------------
-# Creates a class method for each action on each type of fieldmapper object
-# -------------------------------------------------------------------------
 __editor_loaded = False
 def oilsLoadCSEditor():
+    """
+    Creates a class method for each action on each type of fieldmapper object
+    """
+
     global __editor_loaded
     if __editor_loaded:
         return
     __editor_loaded = True
 
-    obj = IDLParser.get_parser().IDLObject
+    obj = IDLParser.get_parser().idl_object
 
-    for k, fm in obj.iteritems():
+    for fmap in obj.itervalues():
         for action in ACTIONS:
 
-            fmname = fm.fieldmapper.replace('::', '_')
-            type = fm.fieldmapper.replace('::', '.')
+            fmname = fmap.fieldmapper.replace('::', '_')
+            obj_type = fmap.fieldmapper.replace('::', '.')
             name = "%s_%s" % (action, fmname)
 
-            s = 'def %s(self, arg, **options):\n' % name
-            s += '\treturn self.runMethod("%s", "%s", arg, dict(options))\n' % (action, type)
-            s += 'setattr(CSEditor, "%s", %s)' % (name, name)
+            method = 'def %s(self, arg, **options):\n' % name
+            method += '\treturn self.runMethod("%s", "%s"' % (action, obj_type)
+            method += ', arg, dict(options))\n'
+            method += 'setattr(CSEditor, "%s", %s)' % (name, name)
 
-            exec(s)
+            exec(method)
 
index e875dd0..fd6f3e2 100644 (file)
@@ -12,15 +12,17 @@ Typical usage:
 ... print oils.utils.idl.IDLParser.get_class('bre').tablename
 biblio.record_entry
 """
-import sys, string, xml.dom.minidom
+import xml.dom.minidom
 #import osrf.net_obj, osrf.log, osrf.set, osrf.ex, osrf.ses
 import osrf.net_obj, osrf.log, osrf.ex, osrf.ses
 from oils.const import OILS_NS_OBJ, OILS_NS_PERSIST, OILS_NS_REPORTER, OILS_APP_ACTOR
 
 class IDLException(osrf.ex.OSRFException):
+    """Exception thrown when parsing the IDL file"""
     pass
 
 class IDLParser(object):
+    """Evergreen fieldmapper IDL file parser"""
 
     # ------------------------------------------------------------
     # static methods and variables for managing a global parser
@@ -43,8 +45,8 @@ class IDLParser(object):
             parser = IDLParser()
             idl_path = osrf.ses.ClientSession.atomic_request(
                 OILS_APP_ACTOR, 'opensrf.open-ils.fetch_idl.file')
-            parser.set_IDL(idl_path)
-            parser.parse_IDL()
+            parser.set_idl(idl_path)
+            parser.parse_idl()
             IDLParser._global_parser = parser
 
     @staticmethod
@@ -53,36 +55,37 @@ class IDLParser(object):
             network hint / IDL class name.
             @param The class ID from the IDL
             '''
-        return IDLParser.get_parser().IDLObject[class_name]
+        return IDLParser.get_parser().idl_object[class_name]
 
     # ------------------------------------------------------------
     # instance methods
     # ------------------------------------------------------------
 
     def __init__(self):
-        self.IDLObject = {}
+        """Initializes the IDL object"""
+        self.idl_object = {}
+        self.idl_file = None
 
-    def set_IDL(self, file):
-        self.idlFile = file
+    def set_IDL(self, idlfile):
+        """Deprecated non-PEP8 version of set_idl()"""
+        self.set_idl(idlfile)
 
-    def _get_attr(self, node, name, ns=None):
-        """ Find the attribute value on a given node 
-            Namespace is ignored for now.. 
-            not sure if minidom has namespace support.
-            """
-        attr = node.attributes.get(name)
-        if attr:
-            return attr.nodeValue
-        return None
+    def set_idl(self, idlfile):
+        """Specifies the filename or file that contains the IDL"""
+        self.idl_file = idlfile
 
     def parse_IDL(self):
+        """Deprecated non-PEP8 version of parse_idl()"""
+        self.parse_idl()
+
+    def parse_idl(self):
         """Parses the IDL file and builds class, field, and link objects"""
 
-        # in case we're calling parse_IDL directly
+        # in case we're calling parse_idl directly
         if not IDLParser._global_parser:
             IDLParser._global_parser = self
 
-        doc = xml.dom.minidom.parse(self.idlFile)
+        doc = xml.dom.minidom.parse(self.idl_file)
         root = doc.documentElement
 
         for child in root.childNodes:
@@ -95,119 +98,67 @@ class IDLParser(object):
                 # -----------------------------------------------------------------------
 
                 obj = IDLClass(
-                    self._get_attr(child, 'id'),
-                    controller = self._get_attr(child, 'controller'),
-                    fieldmapper = self._get_attr(child, 'oils_obj:fieldmapper', OILS_NS_OBJ),
-                    virtual = self._get_attr(child, 'oils_persist:virtual', OILS_NS_PERSIST),
-                    label = self._get_attr(child, 'reporter:label', OILS_NS_REPORTER),
-                    tablename = self._get_attr(child, 'oils_persist:tablename', OILS_NS_REPORTER),
+                    _attr(child, 'id'),
+                    controller = _attr(child, 'controller'),
+                    fieldmapper = _attr(child, 'oils_obj:fieldmapper', OILS_NS_OBJ),
+                    virtual = _attr(child, 'oils_persist:virtual', OILS_NS_PERSIST),
+                    label = _attr(child, 'reporter:label', OILS_NS_REPORTER),
+                    tablename = _attr(child, 'oils_persist:tablename', OILS_NS_PERSIST),
+                    field_safe = _attr(child, 'oils_persist:field_safe', OILS_NS_PERSIST),
                 )
 
-
-                self.IDLObject[obj.name] = obj
+                self.idl_object[obj.name] = obj
 
                 fields = [f for f in child.childNodes if f.nodeName == 'fields']
                 links = [f for f in child.childNodes if f.nodeName == 'links']
 
-                fields = self.parse_fields(obj, fields[0])
+                fields = _parse_fields(obj, fields[0])
                 if len(links) > 0:
-                    self.parse_links(obj, links[0])
+                    _parse_links(obj, links[0])
 
-                osrf.net_obj.register_hint(obj.name, [f.name for f in fields], 'array')
+                osrf.net_obj.register_hint(
+                    obj.name, [f.name for f in fields], 'array'
+                )
 
         doc.unlink()
 
 
-    def parse_links(self, idlobj, links):
-
-        for link in [l for l in links.childNodes if l.nodeName == 'link']:
-            obj = IDLLink(
-                field = idlobj.get_field(self._get_attr(link, 'field')),
-                reltype = self._get_attr(link, 'reltype'),
-                key = self._get_attr(link, 'key'),
-                map = self._get_attr(link, 'map'),
-                class_ = self._get_attr(link, 'class')
-            )
-            idlobj.links.append(obj)
-
-
-    def parse_fields(self, idlobj, fields):
-        """Takes the fields node and parses the included field elements"""
-
-        idlobj.primary = self._get_attr(fields, 'oils_persist:primary', OILS_NS_PERSIST)
-        idlobj.sequence =  self._get_attr(fields, 'oils_persist:sequence', OILS_NS_PERSIST)
-
-        position = 0
-        for field in [l for l in fields.childNodes if l.nodeName == 'field']:
-
-            name = self._get_attr(field, 'name')
-
-            if name in ['isnew', 'ischanged', 'isdeleted']: 
-                continue
-
-            obj = IDLField(
-                idlobj,
-                name = name,
-                position = position,
-                virtual = self._get_attr(field, 'oils_persist:virtual', OILS_NS_PERSIST),
-                label = self._get_attr(field, 'reporter:label', OILS_NS_REPORTER),
-                rpt_datatype = self._get_attr(field, 'reporter:datatype', OILS_NS_REPORTER),
-                rpt_select = self._get_attr(field, 'reporter:selector', OILS_NS_REPORTER),
-                primitive = self._get_attr(field, 'oils_persist:primitive', OILS_NS_PERSIST)
-            )
-
-            idlobj.fields.append(obj)
-            idlobj.field_map[obj.name] = obj
-            position += 1
-
-        for name in ['isnew', 'ischanged', 'isdeleted']: 
-            obj = IDLField(idlobj, 
-                name = name, 
-                position = position, 
-                virtual = 'true'
-            )
-            idlobj.fields.append(obj)
-            position += 1
-
-        return idlobj.fields
-
-
-
 class IDLClass(object):
+    """Represents a class in the fieldmapper IDL"""
+
     def __init__(self, name, **kwargs):
         self.name = name
         self.controller = kwargs.get('controller')
         self.fieldmapper = kwargs.get('fieldmapper')
-        self.virtual = kwargs.get('virtual')
+        self.virtual = _to_bool(kwargs.get('virtual'))
         self.label = kwargs.get('label')
         self.tablename = kwargs.get('tablename')
         self.primary = kwargs.get('primary')
         self.sequence = kwargs.get('sequence')
+        self.field_safe = _to_bool(kwargs.get('field_safe'))
         self.fields = []
         self.links = []
         self.field_map = {}
 
-        if self.virtual and self.virtual.lower() == 'true':
-            self.virtual = True
-        else:
-            self.virtual = False
-
     def __str__(self):
         ''' Stringify the parsed IDL ''' # TODO: improve the format/content
 
-        s = '-'*60 + '\n'
-        s += "%s [%s] %s\n" % (self.label, self.name, self.tablename)
-        s += '-'*60 + '\n'
+        idl = '-'*60 + '\n'
+        idl += "%s [%s] %s\n" % (self.label, self.name, self.tablename)
+        idl += '-'*60 + '\n'
         idx = 0
-        for f in self.fields:
-            s += "[%d] " % idx
-            if idx < 10: s += " "
-            s += str(f) + '\n'
+        for field in self.fields:
+            idl += "[%d] " % idx
+            if idx < 10:
+                idl += " "
+            idl += str(field) + '\n'
             idx += 1
 
-        return s
+        return idl
 
     def get_field(self, field_name):
+        """Return the specified field from the class"""
+
         try:
             return self.field_map[field_name]
         except:
@@ -216,6 +167,8 @@ class IDLClass(object):
             #raise IDLException(msg)
 
 class IDLField(object):
+    """Represents a field in a class in the fieldmapper IDL"""
+
     def __init__(self, idl_class, **kwargs):
         '''
             @param idl_class The IDLClass object which owns this field
@@ -229,25 +182,30 @@ class IDLField(object):
         self.virtual = kwargs.get('virtual')
         self.position = kwargs.get('position')
 
-        if self.virtual and self.virtual.lower() == 'true':
+        if self.virtual and str(self.virtual).lower() == 'true':
             self.virtual = True
         else:
             self.virtual = False
 
     def __str__(self):
         ''' Format as field name and data type, plus linked class for links. '''
-        s = self.name
+        field = self.name
         if self.rpt_datatype:
-            s += " [" + self.rpt_datatype
+            field += " [" + self.rpt_datatype
             if self.rpt_datatype == 'link':
-                link = [ l for l in self.idl_class.links if l.field.name == self.name ]
+                link = [ 
+                    l for l in self.idl_class.links
+                        if l.field.name == self.name 
+                ]
                 if len(link) > 0 and link[0].class_:
-                    s += " @%s" % link[0].class_
-            s += ']'
-        return s
+                    field += " @%s" % link[0].class_
+            field += ']'
+        return field
 
 
 class IDLLink(object):
+    """Represents a link between objects defined in the IDL"""
+
     def __init__(self, field, **kwargs):
         '''
             @param field The IDLField object this link references
@@ -257,3 +215,74 @@ class IDLLink(object):
         self.key = kwargs.get('key')
         self.map = kwargs.get('map')
         self.class_ = kwargs.get('class_')
+
+def _attr(node, name, namespace=None):
+    """ Find the attribute value on a given node 
+        Namespace is ignored for now;
+        not sure if minidom has namespace support.
+        """
+    attr = node.attributes.get(name)
+    if attr:
+        return attr.nodeValue
+    return None
+
+def _parse_links(idlobj, links):
+    """Parses the links between objects defined in the IDL"""
+
+    for link in [l for l in links.childNodes if l.nodeName == 'link']:
+        obj = IDLLink(
+            field = idlobj.get_field(_attr(link, 'field')),
+            reltype = _attr(link, 'reltype'),
+            key = _attr(link, 'key'),
+            map = _attr(link, 'map'),
+            class_ = _attr(link, 'class')
+        )
+        idlobj.links.append(obj)
+
+def _parse_fields(idlobj, fields):
+    """Takes the fields node and parses the included field elements"""
+
+    idlobj.primary = _attr(fields, 'oils_persist:primary', OILS_NS_PERSIST)
+    idlobj.sequence =  _attr(fields, 'oils_persist:sequence', OILS_NS_PERSIST)
+
+    position = 0
+    for field in [l for l in fields.childNodes if l.nodeName == 'field']:
+
+        name = _attr(field, 'name')
+
+        if name in ['isnew', 'ischanged', 'isdeleted']: 
+            continue
+
+        obj = IDLField(
+            idlobj,
+            name = name,
+            position = position,
+            virtual = _attr(field, 'oils_persist:virtual', OILS_NS_PERSIST),
+            label = _attr(field, 'reporter:label', OILS_NS_REPORTER),
+            rpt_datatype = _attr(field, 'reporter:datatype', OILS_NS_REPORTER),
+            rpt_select = _attr(field, 'reporter:selector', OILS_NS_REPORTER),
+            primitive = _attr(field, 'oils_persist:primitive', OILS_NS_PERSIST)
+        )
+
+        idlobj.fields.append(obj)
+        idlobj.field_map[obj.name] = obj
+        position += 1
+
+    for name in ['isnew', 'ischanged', 'isdeleted']: 
+        obj = IDLField(idlobj, 
+            name = name, 
+            position = position, 
+            virtual = 'true'
+        )
+        idlobj.fields.append(obj)
+        position += 1
+
+    return idlobj.fields
+
+def _to_bool(field):
+    """Converts a string from the DOM into a boolean value. """
+
+    if field and str(field).lower() == 'true':
+        return True
+    return False
+
index 12b3448..367ea99 100644 (file)
@@ -1,3 +1,7 @@
+"""
+Grab-bag of general utility functions
+"""
+
 # -----------------------------------------------------------------------
 # Copyright (C) 2007  Georgia Public Library Service
 # Bill Erickson <billserickson@gmail.com>
 # GNU General Public License for more details.
 # -----------------------------------------------------------------------
 
-import re, hashlib
-import osrf.ses
-from osrf.log import *
-
+import hashlib
+import osrf.log, osrf.ses
 
+def md5sum(string):
+    """
+    Return an MD5 message digest for a given input string
+    """
 
-# -----------------------------------------------------------------------
-# Grab-bag of general utility functions
-# -----------------------------------------------------------------------
-
-def md5sum(str):
-    m = hashlib.md5()
-    m.update(str)
-    return m.hexdigest()
+    md5 = hashlib.md5()
+    md5.update(string)
+    return md5.hexdigest()
 
 def unique(arr):
-    ''' Unique-ify a list.  only works if list items are hashable '''
+    """
+    Unique-ify a list.  only works if list items are hashable
+    """
+
     o = {}
     for x in arr:
         o[x] = 1
     return o.keys()
 
 def is_db_true(data):
-    ''' Returns true if the data provided matches what the database considers a true value '''
+    """
+    Returns PostgreSQL's definition of "truth" for the supplied data, roughly.
+    """
+
     if not data or data == 'f' or str(data) == '0':
         return False
     return True
 
+def login(username, password, login_type=None, workstation=None):
+    """
+    Login to the server and get back an authentication token
+
+    @param username: user name
+    @param password: password
+    @param login_type: one of 'opac', 'temp', or 'staff' (default: 'staff')
+    @param workstation: name of the workstation to associate with this login
 
-def login(username, password, type=None, workstation=None):
-    ''' Login to the server and get back an authtoken'''
+    @rtype: string
+    @return: a string containing an authentication token to pass as
+        a required parameter of many OpenSRF service calls
+    """
 
-    log_info("attempting login with user " + username)
+    osrf.log.log_info("attempting login with user " + username)
 
     seed = osrf.ses.ClientSession.atomic_request(
         'open-ils.auth', 
@@ -60,7 +77,7 @@ def login(username, password, type=None, workstation=None):
         {   'workstation' : workstation,
             'username' : username,
             'password' : password,
-            'type' : type
+            'type' : login_type
         }
     )
 
index af763c3..af39c34 100644 (file)
@@ -70,7 +70,7 @@ CREATE TABLE config.upgrade_log (
     install_date    TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
 );
 
-INSERT INTO config.upgrade_log (version) VALUES ('0502'); -- dbwells
+INSERT INTO config.upgrade_log (version) VALUES ('0503'); -- miker for tsbere
 
 CREATE TABLE config.bib_source (
        id              SERIAL  PRIMARY KEY,
@@ -420,7 +420,8 @@ CREATE TABLE config.rule_recurring_fine (
        high                    NUMERIC(6,2)    NOT NULL,
        normal                  NUMERIC(6,2)    NOT NULL,
        low                     NUMERIC(6,2)    NOT NULL,
-       recurrence_interval     INTERVAL        NOT NULL DEFAULT '1 day'::INTERVAL
+       recurrence_interval     INTERVAL        NOT NULL DEFAULT '1 day'::INTERVAL,
+    grace_period       INTERVAL         NOT NULL DEFAULT '1 day'::INTERVAL
 );
 COMMENT ON TABLE config.rule_recurring_fine IS $$
 /*
index cb7391a..3a34659 100644 (file)
@@ -109,6 +109,7 @@ CREATE TABLE action.circulation (
        checkin_staff           INT,                                      -- actor.usr.id
        checkin_lib             INT,                                      -- actor.org_unit.id
        renewal_remaining       INT                             NOT NULL, -- derived from "circ duration" rule
+    grace_period           INTERVAL             NOT NULL, -- derived from "circ fine" rule
        due_date                TIMESTAMP WITH TIME ZONE,
        stop_fines_time         TIMESTAMP WITH TIME ZONE,
        checkin_time            TIMESTAMP WITH TIME ZONE,
@@ -191,7 +192,7 @@ CREATE INDEX action_aged_circulation_target_copy_idx ON action.aged_circulation
 CREATE OR REPLACE VIEW action.all_circulation AS
     SELECT  id,usr_post_code, usr_home_ou, usr_profile, usr_birth_year, copy_call_number, copy_location,
         copy_owning_lib, copy_circ_lib, copy_bib_record, xact_start, xact_finish, target_copy,
-        circ_lib, circ_staff, checkin_staff, checkin_lib, renewal_remaining, due_date,
+        circ_lib, circ_staff, checkin_staff, checkin_lib, renewal_remaining, grace_period, due_date,
         stop_fines_time, checkin_time, create_time, duration, fine_interval, recurring_fine,
         max_fine, phone_renewal, desk_renewal, opac_renewal, duration_rule, recurring_fine_rule,
         max_fine_rule, stop_fines, workstation, checkin_workstation, checkin_scan_time, parent_circ
@@ -200,7 +201,7 @@ CREATE OR REPLACE VIEW action.all_circulation AS
     SELECT  DISTINCT circ.id,COALESCE(a.post_code,b.post_code) AS usr_post_code, p.home_ou AS usr_home_ou, p.profile AS usr_profile, EXTRACT(YEAR FROM p.dob)::INT AS usr_birth_year,
         cp.call_number AS copy_call_number, cp.location AS copy_location, cn.owning_lib AS copy_owning_lib, cp.circ_lib AS copy_circ_lib,
         cn.record AS copy_bib_record, circ.xact_start, circ.xact_finish, circ.target_copy, circ.circ_lib, circ.circ_staff, circ.checkin_staff,
-        circ.checkin_lib, circ.renewal_remaining, circ.due_date, circ.stop_fines_time, circ.checkin_time, circ.create_time, circ.duration,
+        circ.checkin_lib, circ.renewal_remaining, circ.grace_period, circ.due_date, circ.stop_fines_time, circ.checkin_time, circ.create_time, circ.duration,
         circ.fine_interval, circ.recurring_fine, circ.max_fine, circ.phone_renewal, circ.desk_renewal, circ.opac_renewal, circ.duration_rule,
         circ.recurring_fine_rule, circ.max_fine_rule, circ.stop_fines, circ.workstation, circ.checkin_workstation, circ.checkin_scan_time,
         circ.parent_circ
@@ -233,14 +234,14 @@ BEGIN
     INSERT INTO action.aged_circulation
         (id,usr_post_code, usr_home_ou, usr_profile, usr_birth_year, copy_call_number, copy_location,
         copy_owning_lib, copy_circ_lib, copy_bib_record, xact_start, xact_finish, target_copy,
-        circ_lib, circ_staff, checkin_staff, checkin_lib, renewal_remaining, due_date,
+        circ_lib, circ_staff, checkin_staff, checkin_lib, renewal_remaining, grace_period, due_date,
         stop_fines_time, checkin_time, create_time, duration, fine_interval, recurring_fine,
         max_fine, phone_renewal, desk_renewal, opac_renewal, duration_rule, recurring_fine_rule,
         max_fine_rule, stop_fines, workstation, checkin_workstation, checkin_scan_time, parent_circ)
       SELECT
         id,usr_post_code, usr_home_ou, usr_profile, usr_birth_year, copy_call_number, copy_location,
         copy_owning_lib, copy_circ_lib, copy_bib_record, xact_start, xact_finish, target_copy,
-        circ_lib, circ_staff, checkin_staff, checkin_lib, renewal_remaining, due_date,
+        circ_lib, circ_staff, checkin_staff, checkin_lib, renewal_remaining, grace_period, due_date,
         stop_fines_time, checkin_time, create_time, duration, fine_interval, recurring_fine,
         max_fine, phone_renewal, desk_renewal, opac_renewal, duration_rule, recurring_fine_rule,
         max_fine_rule, stop_fines, workstation, checkin_workstation, checkin_scan_time, parent_circ
index c4e3b5b..a026d26 100644 (file)
@@ -75,6 +75,7 @@ CREATE TABLE config.circ_matrix_matchpoint (
     max_fine_rule        INT     REFERENCES config.rule_max_fine (id) DEFERRABLE INITIALLY DEFERRED,
     hard_due_date        INT     REFERENCES config.hard_due_date (id) DEFERRABLE INITIALLY DEFERRED,
     renewals             INT,    -- Renewal count override
+    grace_period         INTERVAL,    -- Grace period override
     script_test          TEXT,                           -- javascript source 
     total_copy_hold_ratio     FLOAT,
     available_copy_hold_ratio FLOAT
@@ -249,6 +250,9 @@ BEGIN
         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
@@ -337,7 +341,7 @@ BEGIN
 END;
 $func$ LANGUAGE PLPGSQL;
 
-CREATE TYPE action.circ_matrix_test_result AS ( success BOOL, fail_part TEXT, buildrows INT[], matchpoint INT, circulate BOOL, duration_rule INT, recurring_fine_rule INT, max_fine_rule INT, hard_due_date INT, renewals INT );
+CREATE TYPE action.circ_matrix_test_result AS ( success BOOL, fail_part TEXT, buildrows INT[], matchpoint INT, circulate BOOL, duration_rule INT, recurring_fine_rule INT, max_fine_rule INT, hard_due_date INT, renewals INT, grace_period INTERVAL );
 CREATE OR REPLACE FUNCTION action.item_user_circ_test( circ_ou INT, match_item BIGINT, match_user INT, renewal BOOL ) RETURNS SETOF action.circ_matrix_test_result AS $func$
 DECLARE
     user_object             actor.usr%ROWTYPE;
@@ -429,6 +433,7 @@ BEGIN
     result.max_fine_rule        := circ_matchpoint.max_fine_rule;
     result.hard_due_date        := circ_matchpoint.hard_due_date;
     result.renewals             := circ_matchpoint.renewals;
+    result.grace_period         := circ_matchpoint.grace_period;
     result.buildrows            := circ_test.buildrows;
 
     -- Fail if we couldn't find a matchpoint
index 0c33834..bad385a 100644 (file)
@@ -194,11 +194,11 @@ INSERT INTO config.rule_max_fine VALUES
 SELECT SETVAL('config.rule_max_fine_id_seq'::TEXT, 100);
 
 INSERT INTO config.rule_recurring_fine VALUES 
-    (1, oils_i18n_gettext(1, 'default', 'crrf', 'name'), 0.50, 0.10, 0.05, '1 day');
+    (1, oils_i18n_gettext(1, 'default', 'crrf', 'name'), 0.50, 0.10, 0.05, '1 day', '1 day');
 INSERT INTO config.rule_recurring_fine VALUES 
-    (2, oils_i18n_gettext(2, '10_cent_per_day', 'crrf', 'name'), 0.50, 0.10, 0.10, '1 day');
+    (2, oils_i18n_gettext(2, '10_cent_per_day', 'crrf', 'name'), 0.50, 0.10, 0.10, '1 day', '1 day');
 INSERT INTO config.rule_recurring_fine VALUES 
-    (3, oils_i18n_gettext(3, '50_cent_per_day', 'crrf', 'name'), 0.50, 0.50, 0.50, '1 day');
+    (3, oils_i18n_gettext(3, '50_cent_per_day', 'crrf', 'name'), 0.50, 0.50, 0.50, '1 day', '1 day');
 SELECT SETVAL('config.rule_recurring_fine_id_seq'::TEXT, 100);
 
 INSERT INTO config.rule_age_hold_protect VALUES
diff --git a/Open-ILS/src/sql/Pg/upgrade/0503.schema.grace_periods.sql b/Open-ILS/src/sql/Pg/upgrade/0503.schema.grace_periods.sql
new file mode 100644 (file)
index 0000000..05d1bcd
--- /dev/null
@@ -0,0 +1,470 @@
+BEGIN;
+
+-- FAIR WARNING:
+-- Using a tool such as pgadmin to run this script may fail
+-- If it does, try psql command line.
+
+-- Change this to FALSE to disable updating existing circs
+-- Otherwise will use the fine interval for the grace period
+\set CircGrace TRUE
+
+INSERT INTO config.upgrade_log (version) VALUES ('0503');
+
+-- New Columns
+
+ALTER TABLE config.circ_matrix_matchpoint
+    ADD COLUMN grace_period INTERVAL;
+
+ALTER TABLE config.rule_recurring_fine
+    ADD COLUMN grace_period INTERVAL NOT NULL DEFAULT '1 day';
+
+ALTER TABLE action.circulation
+    ADD COLUMN grace_period INTERVAL NOT NULL DEFAULT '0 seconds';
+
+ALTER TABLE action.aged_circulation
+    ADD COLUMN grace_period INTERVAL NOT NULL DEFAULT '0 seconds';
+
+-- Remove defaults needed to stop null complaints
+
+ALTER TABLE action.circulation
+    ALTER COLUMN grace_period DROP DEFAULT;
+
+ALTER TABLE action.aged_circulation
+    ALTER COLUMN grace_period DROP DEFAULT;
+
+-- Drop Views
+
+DROP VIEW action.all_circulation;
+DROP VIEW action.open_circulation;
+DROP VIEW action.billable_circulations;
+
+-- Replace Views
+
+CREATE OR REPLACE VIEW action.all_circulation AS
+    SELECT  id,usr_post_code, usr_home_ou, usr_profile, usr_birth_year, copy_call_number, copy_location,
+        copy_owning_lib, copy_circ_lib, copy_bib_record, xact_start, xact_finish, target_copy,
+        circ_lib, circ_staff, checkin_staff, checkin_lib, renewal_remaining, grace_period, due_date,
+        stop_fines_time, checkin_time, create_time, duration, fine_interval, recurring_fine,
+        max_fine, phone_renewal, desk_renewal, opac_renewal, duration_rule, recurring_fine_rule,
+        max_fine_rule, stop_fines, workstation, checkin_workstation, checkin_scan_time, parent_circ
+      FROM  action.aged_circulation
+            UNION ALL
+    SELECT  DISTINCT circ.id,COALESCE(a.post_code,b.post_code) AS usr_post_code, p.home_ou AS usr_home_ou, p.profile AS usr_profile, EXTRACT(YEAR FROM p.dob)::INT AS usr_birth_year,
+        cp.call_number AS copy_call_number, cp.location AS copy_location, cn.owning_lib AS copy_owning_lib, cp.circ_lib AS copy_circ_lib,
+        cn.record AS copy_bib_record, circ.xact_start, circ.xact_finish, circ.target_copy, circ.circ_lib, circ.circ_staff, circ.checkin_staff,
+        circ.checkin_lib, circ.renewal_remaining, circ.grace_period, circ.due_date, circ.stop_fines_time, circ.checkin_time, circ.create_time, circ.duration,
+        circ.fine_interval, circ.recurring_fine, circ.max_fine, circ.phone_renewal, circ.desk_renewal, circ.opac_renewal, circ.duration_rule,
+        circ.recurring_fine_rule, circ.max_fine_rule, circ.stop_fines, circ.workstation, circ.checkin_workstation, circ.checkin_scan_time,
+        circ.parent_circ
+      FROM  action.circulation circ
+        JOIN asset.copy cp ON (circ.target_copy = cp.id)
+        JOIN asset.call_number cn ON (cp.call_number = cn.id)
+        JOIN actor.usr p ON (circ.usr = p.id)
+        LEFT JOIN actor.usr_address a ON (p.mailing_address = a.id)
+        LEFT JOIN actor.usr_address b ON (p.billing_address = a.id);
+
+CREATE OR REPLACE VIEW action.open_circulation AS
+       SELECT  *
+         FROM  action.circulation
+         WHERE checkin_time IS NULL
+         ORDER BY due_date;
+               
+
+CREATE OR REPLACE VIEW action.billable_circulations AS
+       SELECT  *
+         FROM  action.circulation
+         WHERE xact_finish IS NULL;
+
+-- Drop Functions that rely on types
+
+DROP FUNCTION action.item_user_circ_test(INT, BIGINT, INT, BOOL);
+DROP FUNCTION action.item_user_circ_test(INT, BIGINT, INT);
+DROP FUNCTION action.item_user_renew_test(INT, BIGINT, INT);
+
+-- Drop Types that are changing
+
+DROP TYPE action.circ_matrix_test_result;
+
+-- Replace Types
+
+CREATE TYPE action.circ_matrix_test_result AS ( success BOOL, fail_part TEXT, buildrows INT[], matchpoint INT, circulate BOOL, duration_rule INT, recurring_fine_rule INT, max_fine_rule INT, hard_due_date INT, renewals INT, grace_period INTERVAL );
+
+-- Fix/Replace Functions
+
+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;
+    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;
+
+    -- 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_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;
+    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_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)
+          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 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.item_user_circ_test( circ_ou INT, match_item BIGINT, match_user INT, renewal BOOL ) RETURNS SETOF action.circ_matrix_test_result AS $func$
+DECLARE
+    user_object             actor.usr%ROWTYPE;
+    standing_penalty        config.standing_penalty%ROWTYPE;
+    item_object             asset.copy%ROWTYPE;
+    item_status_object      config.copy_status%ROWTYPE;
+    item_location_object    asset.copy_location%ROWTYPE;
+    result                  action.circ_matrix_test_result;
+    circ_test               action.found_circ_matrix_matchpoint;
+    circ_matchpoint         config.circ_matrix_matchpoint%ROWTYPE;
+    out_by_circ_mod         config.circ_matrix_circ_mod_test%ROWTYPE;
+    circ_mod_map            config.circ_matrix_circ_mod_test_map%ROWTYPE;
+    hold_ratio              action.hold_stats%ROWTYPE;
+    penalty_type            TEXT;
+    items_out               INT;
+    context_org_list        INT[];
+    done                    BOOL := FALSE;
+BEGIN
+    -- Assume success unless we hit a failure condition
+    result.success := TRUE;
+
+    -- Fail if the user is BARRED
+    SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
+
+    -- Fail if we couldn't find the 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 the item 
+    IF item_object.id IS NULL THEN
+        result.fail_part := 'no_item';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+        RETURN;
+    END IF;
+
+    IF user_object.barred IS TRUE THEN
+        result.fail_part := 'actor.usr.barred';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    END IF;
+
+    -- Fail if the item can't circulate
+    IF item_object.circulate IS FALSE THEN
+        result.fail_part := 'asset.copy.circulate';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    END IF;
+
+    -- Fail if the item isn't in a circulateable status on a non-renewal
+    IF NOT renewal AND item_object.status NOT IN ( 0, 7, 8 ) THEN 
+        result.fail_part := 'asset.copy.status';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    ELSIF renewal AND item_object.status <> 1 THEN
+        result.fail_part := 'asset.copy.status';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    END IF;
+
+    -- Fail if the item can't circulate because of the shelving location
+    SELECT INTO item_location_object * FROM asset.copy_location WHERE id = item_object.location;
+    IF item_location_object.circulate IS FALSE THEN
+        result.fail_part := 'asset.copy_location.circulate';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    END IF;
+
+    SELECT INTO circ_test * FROM action.find_circ_matrix_matchpoint(circ_ou, item_object, user_object, renewal);
+
+    circ_matchpoint             := circ_test.matchpoint;
+    result.matchpoint           := circ_matchpoint.id;
+    result.circulate            := circ_matchpoint.circulate;
+    result.duration_rule        := circ_matchpoint.duration_rule;
+    result.recurring_fine_rule  := circ_matchpoint.recurring_fine_rule;
+    result.max_fine_rule        := circ_matchpoint.max_fine_rule;
+    result.hard_due_date        := circ_matchpoint.hard_due_date;
+    result.renewals             := circ_matchpoint.renewals;
+    result.grace_period         := circ_matchpoint.grace_period;
+    result.buildrows            := circ_test.buildrows;
+
+    -- Fail if we couldn't find a matchpoint
+    IF circ_test.success = false THEN
+        result.fail_part := 'no_matchpoint';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+        RETURN; -- All tests after this point require a matchpoint. No sense in running on an incomplete or missing one.
+    END IF;
+
+    -- Apparently....use the circ matchpoint org unit to determine what org units are valid.
+    SELECT INTO context_org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( circ_matchpoint.org_unit );
+
+    IF renewal THEN
+        penalty_type = '%RENEW%';
+    ELSE
+        penalty_type = '%CIRC%';
+    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 unnest(context_org_list) )
+                AND (usp.stop_date IS NULL or usp.stop_date > NOW())
+                AND csp.block_list LIKE penalty_type LOOP
+
+        result.fail_part := standing_penalty.name;
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    END LOOP;
+
+    -- Fail if the test is set to hard non-circulating
+    IF circ_matchpoint.circulate IS FALSE THEN
+        result.fail_part := 'config.circ_matrix_test.circulate';
+        result.success := FALSE;
+        done := TRUE;
+        RETURN NEXT result;
+    END IF;
+
+    -- Fail if the total copy-hold ratio is too low
+    IF circ_matchpoint.total_copy_hold_ratio IS NOT NULL THEN
+        SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
+        IF hold_ratio.total_copy_ratio IS NOT NULL AND hold_ratio.total_copy_ratio < circ_matchpoint.total_copy_hold_ratio THEN
+            result.fail_part := 'config.circ_matrix_test.total_copy_hold_ratio';
+            result.success := FALSE;
+            done := TRUE;
+            RETURN NEXT result;
+        END IF;
+    END IF;
+
+    -- Fail if the available copy-hold ratio is too low
+    IF circ_matchpoint.available_copy_hold_ratio IS NOT NULL THEN
+        IF hold_ratio.hold_count IS NULL THEN
+            SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
+        END IF;
+        IF hold_ratio.available_copy_ratio IS NOT NULL AND hold_ratio.available_copy_ratio < circ_matchpoint.available_copy_hold_ratio THEN
+            result.fail_part := 'config.circ_matrix_test.available_copy_hold_ratio';
+            result.success := FALSE;
+            done := TRUE;
+            RETURN NEXT result;
+        END IF;
+    END IF;
+
+    -- Fail if the user has too many items with specific circ_modifiers checked out
+    FOR out_by_circ_mod IN SELECT * FROM config.circ_matrix_circ_mod_test WHERE matchpoint = circ_matchpoint.id LOOP
+        SELECT  INTO items_out COUNT(*)
+          FROM  action.circulation circ
+            JOIN asset.copy cp ON (cp.id = circ.target_copy)
+          WHERE circ.usr = match_user
+               AND circ.circ_lib IN ( SELECT * FROM unnest(context_org_list) )
+            AND circ.checkin_time IS NULL
+            AND (circ.stop_fines IN ('MAXFINES','LONGOVERDUE') OR circ.stop_fines IS NULL)
+            AND cp.circ_modifier IN (SELECT circ_mod FROM config.circ_matrix_circ_mod_test_map WHERE circ_mod_test = out_by_circ_mod.id);
+        IF items_out >= out_by_circ_mod.items_out THEN
+            result.fail_part := 'config.circ_matrix_circ_mod_test';
+            result.success := FALSE;
+            done := TRUE;
+            RETURN NEXT result;
+        END IF;
+    END LOOP;
+
+    -- If we passed everything, return the successful matchpoint id
+    IF NOT done THEN
+        RETURN NEXT result;
+    END IF;
+
+    RETURN;
+END;
+$func$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION action.item_user_circ_test( INT, BIGINT, INT ) RETURNS SETOF action.circ_matrix_test_result AS $func$
+    SELECT * FROM action.item_user_circ_test( $1, $2, $3, FALSE );
+$func$ LANGUAGE SQL;
+
+CREATE OR REPLACE FUNCTION action.item_user_renew_test( INT, BIGINT, INT ) RETURNS SETOF action.circ_matrix_test_result AS $func$
+    SELECT * FROM action.item_user_circ_test( $1, $2, $3, TRUE );
+$func$ LANGUAGE SQL;
+
+-- Update recurring fine rules
+UPDATE config.rule_recurring_fine SET grace_period=recurrence_interval;
+
+-- Update Circulation Data
+-- Only update if we were told to and the circ hasn't been checked in
+UPDATE action.circulation SET grace_period=fine_interval WHERE :CircGrace AND (checkin_time IS NULL);
+
+COMMIT;
index 8859b13..3ab838a 100755 (executable)
@@ -1,7 +1,7 @@
 #!/usr/bin/perl
 # ---------------------------------------------------------------------
-# Fine generator with default grace period param.
-# ./object_dumper.pl <bootstrap_config> <lockfile> <grace (default 0)>
+# Fine generator
+# ./fine_generator.pl <bootstrap_config> <lockfile>
 # ---------------------------------------------------------------------
 
 use strict; 
@@ -15,7 +15,9 @@ my $config = shift || die "bootstrap config required\n";
 my $lockfile = shift || "/tmp/generate_fines-LOCK";
 my $grace = shift;
 
-$grace = '' if (!defined($grace) or $grace == 0);
+if (defined($grace)) {
+    die "Grace period is now defined in the database. It should not be passed to the fine generator.";
+}
  
 if (-e $lockfile) {
         open(F,$lockfile);
@@ -44,7 +46,7 @@ if ($parallel == 1) {
 
     my $r = OpenSRF::AppSession
             ->create( 'open-ils.storage' )
-            ->request( 'open-ils.storage.action.circulation.overdue.generate_fines' => $grace );
+            ->request( 'open-ils.storage.action.circulation.overdue.generate_fines' );
 
     while (!$r->complete) { $r->recv };
 
@@ -57,10 +59,10 @@ if ($parallel == 1) {
     );
 
     my $storage = OpenSRF::AppSession->create("open-ils.storage");
-    my $r = $storage->request('open-ils.storage.action.circulation.overdue.id_list', $grace);
+    my $r = $storage->request('open-ils.storage.action.circulation.overdue.id_list');
     while (my $resp = $r->recv) {
         my $circ_id = $resp->content;
-        $multi_generator->request( 'open-ils.storage.action.circulation.overdue.generate_fines', $grace, $circ_id );
+        $multi_generator->request( 'open-ils.storage.action.circulation.overdue.generate_fines', $circ_id );
     }
     $storage->disconnect();
     $multi_generator->session_wait(1);
index f5d5b3a..6e8dd50 100644 (file)
@@ -23,6 +23,7 @@ function load(){
     cmGrid.overrideWidgetArgs.available_copy_hold_ratio = {inherits : true};
     cmGrid.overrideWidgetArgs.total_copy_hold_ratio = {inherits : true};
     cmGrid.overrideWidgetArgs.renewals = {inherits : true};
+    cmGrid.overrideWidgetArgs.grace_period = {inherits : true};
     cmGrid.overrideWidgetArgs.hard_due_date = {inherits : true};
     cmGrid.loadAll({order_by:{ccmm:'circ_modifier'}});
     cmGrid.onEditPane = buildEditPaneAdditions;
index dda4906..b699f46 100644 (file)
@@ -1043,8 +1043,13 @@ function _timerRun(tname) {
 }
 
 function checkILSEvent(obj) {
-       if( obj && obj.ilsevent != null && obj.ilsevent != 0 )
-               return parseInt(obj.ilsevent);
+       if (obj && typeof obj == 'object' && typeof obj.ilsevent != 'undefined') {
+        if (obj.ilsevent === '') {
+            return true;
+        } else if ( obj.ilsevent != null && obj.ilsevent != 0 ) {
+            return parseInt(obj.ilsevent);
+        }
+    }
        return null;
 }
 
index bd5f8c6..9908b2a 100644 (file)
@@ -92,7 +92,7 @@
                 <td type='opac/slot-data' query='datafield[tag^="6"]' class='rdetail_item'>
                     <script type='opac/slot-format'><![CDATA[
                         var cgi = new CGI();
-                        var other_params = [ 'd', 'l', 'r', 'av', 's', 'sd' ];
+                        var other_params = [ 'd', 'l', 'r', 'av', 's', 'sd', 'ol' ];
                         var total = '';
                         var output = [];
                         var list = dojo.query( 'subfield', item );
index c09012f..26b0977 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_vr_format', 'ref_flag', 'usr_age_lower_bound', 'usr_age_upper_bound', 'circulate', 'duration_rule', 'renewals', 'hard_due_date', 'recurring_fine_rule', '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_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']"
             defaultCellWidth='"auto"'
             query="{id: '*'}"
             fmClass='ccmm'
index 47887da..fbb25f6 100644 (file)
@@ -11,7 +11,7 @@
     <div>
     <table  jsId="ruleRecurringFineGrid"
             dojoType="openils.widget.AutoGrid"
-            fieldOrder="['name', 'recurrence_interval', 'low', 'normal', 'high']"
+            fieldOrder="['name', 'recurrence_interval', 'low', 'normal', 'high', 'grace_period']"
             suppressFields="['id']"
             query="{id: '*'}"
             fmClass='crrf'
index a462ec0..5e3a030 100644 (file)
@@ -26,6 +26,8 @@ export NSIS_WICON=$$(if [ -f client/evergreen.ico ]; then echo '-DWICON'; fi)
 export NSIS_AUTOUPDATE=$$([ -f client/defaults/preferences/autoupdate.js ] && echo '-DAUTOUPDATE')
 export NSIS_DEV=$$([ -f client/defaults/preferences/developers.js ] && echo '-DDEVELOPER')
 export NSIS_PERMACHINE=$$([ -f client/defaults/preferences/aa_per_machine.js ] && echo '-DPERMACHINE')
+# Url taken from http://nsis.sourceforge.net/AccessControl_plug-in
+NSIS_ACCESSCONTROL=http://nsis.sourceforge.net/mediawiki/images/4/4a/AccessControl.zip
 
 #------------------------------
 # Build ILS XUL CLIENT/SERVER
@@ -250,8 +252,9 @@ linux-xulrunner: client_app
 # Build a windows installer.
 
 win-client: win-xulrunner
+       @if [ "${NSIS_AUTOUPDATE}${NSIS_PERMACHINE}" -a ! -d AccessControl ]; then echo 'Fetching AccessControl Plugin'; wget ${NSIS_ACCESSCONTROL} -O AccessControl.zip; unzip AccessControl.zip; fi
        @echo 'Building installer'
-       @makensis -DPRODUCT_VERSION="${STAFF_CLIENT_VERSION}" ${NSIS_WICON} ${NSIS_AUTOUPDATE} ${NSIS_DEV} ${NSIS_PERMACHINE} ${NSIS_EXTRAOPTS} windowssetup.nsi
+       @makensis -V2 -DPRODUCT_VERSION="${STAFF_CLIENT_VERSION}" ${NSIS_WICON} ${NSIS_AUTOUPDATE} ${NSIS_DEV} ${NSIS_PERMACHINE} ${NSIS_EXTRAOPTS} windowssetup.nsi
        @echo 'Done'
 
 # For linux, just build a tar.bz2 archive
index 0427ec9..d5077fb 100644 (file)
@@ -113,13 +113,8 @@ Section "Staff Client" SECMAIN
 
   !ifdef AUTOUPDATE | PERMACHINE
   ; For autoupdate and/or registering per machine, make sure we can write to the install directory.
-  ; If the AccessControl plugin was packaged or part of nsis we would use it instead.
-  ; Also, as cacls.exe is depreciated when icacls.exe exists, try icacls.exe first.
-  IfFileExists "$SYSDIR/icacls.exe" 0 +3
-  ExecWait '"$SYSDIR/icacls.exe" "$INSTDIR" /grant Everyone:(OI)(CI)F'  
-  Goto +3
-  IfFileExists "$SYSDIR/cacls.exe" 0 +2
-  ExecWait '"$SYSDIR/cacls.exe" "$INSTDIR" /E /G Everyone:F'
+  !addplugindir AccessControl/Plugins
+  AccessControl::GrantOnFile "$INSTDIR" "Everyone" "FullAccess"
   !endif
 SectionEnd
 
index 1d76bd7..e4130c2 100755 (executable)
@@ -55,7 +55,8 @@ function feedback() {
 
 PSQL_ACCESS="-h $DB_HOST -U $DB_USER $DB_NAME";
 
-VERSION=$(psql -c "select max(version) from config.upgrade_log" -t $PSQL_ACCESS);
+# Need to avoid versions like '1.6.0.4' from throwing off the upgrade
+VERSION=$(psql -c "SELECT MAX(version) FROM config.upgrade_log WHERE version ~ E'^\\\\d+$'" -t $PSQL_ACCESS);
 [  $? -gt 0  ] && die "Database access failed.";
 # [ $VERBOSE ] && echo RAW VERSION: $VERSION     # TODO: for verbose mode
 VERSION=$(echo $VERSION | sed -e 's/^ *0*//');    # This is a separate step so we can check $? above.