From ec5f6b2e2c018815e7b3b8f889540b3eeed21277 Mon Sep 17 00:00:00 2001 From: miker Date: Wed, 16 Feb 2011 20:47:11 +0000 Subject: [PATCH] Patch from Thomas Berezansky to implement circulation matchpoint value fallthrough, whereby specific rules can inherit values from generic rules. Background, taken from https://bugs.launchpad.net/evergreen/+bug/635463: Theory: Make the things returned (circulate, duration_rule, recurring_fine_rule, max_fine_rule, hard_due_date, total_copy_hold_ratio, available_copy_hold_ratio and renewals) "fall through" when set to NULL. The circ matrix edit screen has had some changes too, partially for showing "Inherited" in some places where appropriate, partially for field ordering and editing display and consistency. The fields that show up earlier contribute to tests that will be applied to the circulation attempt, while the "results" that do not change what rows are considered are grouped at the end. Pros: You can override a subset of those fields with a specific rule while allowing broader rules to fill in the holes. This may result in less duplication of information across rules, making things easier to maintain. Thus, this may result in less rules in general, and thus less processing time on sorting them overall. Cons: Manually figuring out the specifics of what will happen will take more time/effort. Changing a single rule may have a greater unintended effect on other rules. Staff would need training for when to have a rule fall through and when to set it specifically. More time to return from the DB for any rule that is "falling through" to broader rules. Examples for the following org tree: CONS -SYSA --LIBC --LIBD -SYSB --LIBE --LIBF Implementing the following "business" rules: At the CONS level: By default, everything circulates, uses DFLT_DUR duration, DFLT_RFINE recurring fine, and DFLT_MFINE max fine. Circ Modifier "book" uses the duration BOOK_DUR Reference flagged materials don't circulate At the SYSA level there are no special rules. At the SYSB level the max fine should be SYSB_MFINE. At the LIBC level the recurring fine is LIBC_RFINE At the LIBD level circ modifier "book" uses the DFLT_DUR duration instead of "BOOK_DUR" At the LIBE level reference flagged materials circulate. At the LIBF level there are no special rules. The current method would require the following circ rules to implement those business rules: CIRC_LIB CIRC_MOD REFERENCE CIRC? DURATION_RULE RECURRING_FINE MAX_FINE CONS NULL NULL TRUE DFLT_DUR DFLT_RFINE DFLT_MFINE CONS NULL TRUE FALSE DFLT_DUR DFLT_RFINE DFLT_MFINE CONS book NULL TRUE BOOK_DUR DFLT_RFINE DFLT_MFINE CONS book TRUE FALSE BOOK_DUR DFLT_RFINE DFLT_MFINE SYSB NULL NULL TRUE DFLT_DUR DFLT_RFINE SYSB_MFINE SYSB NULL TRUE FALSE DFLT_DUR DFLT_RFINE SYSB_MFINE SYSB book NULL TRUE BOOK_DUR DFLT_RFINE SYSB_MFINE SYSB book TRUE FALSE BOOK_DUR DFLT_RFINE SYSB_MFINE LIBC NULL NULL TRUE DFLT_DUR LIBC_RFINE DFLT_MFINE LIBC NULL TRUE FALSE DFLT_DUR LIBC_RFINE DFLT_MFINE LIBC book NULL TRUE BOOK_DUR LIBC_RFINE DFLT_MFINE LIBC book TRUE FALSE BOOK_DUR LIBC_RFINE DFLT_MFINE LIBD book NULL TRUE DFLT_DUR DFLT_RFINE DFLT_MFINE LIBD book TRUE FALSE DFLT_DUR DFLT_RFINE DFLT_MFINE LIBE NULL NULL TRUE DFLT_DUR DFLT_RFINE SYSB_MFINE LIBE book NULL TRUE BOOK_DUR DFLT_RFINE SYSB_MFINE 16 circ rules total. The new method would require the following circ rules to implement those business rules: CIRC_LIB CIRC_MOD REFERENCE CIRC? DURATION_RULE RECURRING_FINE MAX_FINE CONS NULL NULL TRUE DFLT_DUR DFLT_RFINE DFLT_MFINE CONS book NULL NULL BOOK_DUR NULL NULL CONS NULL TRUE FALSE NULL NULL NULL SYSB NULL NULL NULL NULL NULL SYSB_MFINE LIBC NULL NULL NULL NULL LIBC_RFINE NULL LIBD book NULL NULL DFLT_DUR NULL NULL LIBE NULL TRUE TRUE NULL NULL NULL 7 circ rules total. Starting with the above, lets assume that SYSA wants to change their recurring fine to SYSA_RFINE. LIBC's recurring fine is to be unchanged. The current method requires the following changes: ADD the following entries: CIRC_LIB CIRC_MOD REFERENCE CIRC? DURATION_RULE RECURRING_FINE MAX_FINE SYSA NULL NULL TRUE DFLT_DUR SYSA_RFINE DFLT_MFINE SYSA NULL TRUE FALSE DFLT_DUR SYSA_RFINE DFLT_MFINE SYSA book NULL TRUE BOOK_DUR SYSA_RFINE DFLT_MFINE SYSA book TRUE FALSE BOOK_DUR SYSA_RFINE DFLT_MFINE UPDATE the LIBD entries: CIRC_LIB CIRC_MOD REFERENCE CIRC? DURATION_RULE RECURRING_FINE MAX_FINE LIBD book NULL TRUE DFLT_DUR SYSA_RFINE DFLT_MFINE LIBD book TRUE FALSE DFLT_DUR SYSA_RFINE DFLT_MFINE 4 rules added, 2 changed, total is now 20 rules. The new method would require the following changes: ADD the following entry: CIRC_LIB CIRC_MOD REFERENCE CIRC? DURATION_RULE RECURRING_FINE MAX_FINE SYSA NULL NULL NULL NULL SYSA_RFINE NULL 1 rule added, 0 changed, total is now 8 rules. git-svn-id: svn://svn.open-ils.org/ILS/trunk@19453 dcc99617-32d9-48b4-a31d-7c20da2025e4 --- Open-ILS/examples/fm_IDL.xml | 8 +- .../lib/OpenILS/Application/Circ/Circulate.pm | 19 +- Open-ILS/src/sql/Pg/002.schema.config.sql | 2 +- Open-ILS/src/sql/Pg/100.circ_matrix.sql | 320 +++++++++----- Open-ILS/src/sql/Pg/110.hold_matrix.sql | 1 + Open-ILS/src/sql/Pg/950.data.seed-values.sql | 2 +- .../upgrade/0487.circ_matrix_fallthrough.sql | 404 ++++++++++++++++++ .../js/dojo/openils/widget/AutoFieldWidget.js | 12 +- .../web/js/dojo/openils/widget/AutoGrid.js | 6 +- .../web/js/dojo/openils/widget/EditPane.js | 17 +- .../openils/widget/nls/AutoFieldWidget.js | 3 +- .../global/config/circ_matrix_matchpoint.js | 11 + .../global/config/circ_matrix_matchpoint.tt2 | 19 +- 13 files changed, 679 insertions(+), 145 deletions(-) create mode 100644 Open-ILS/src/sql/Pg/upgrade/0487.circ_matrix_fallthrough.sql diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml index 0f130066c2..60e38b8a39 100644 --- a/Open-ILS/examples/fm_IDL.xml +++ b/Open-ILS/examples/fm_IDL.xml @@ -1146,13 +1146,15 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + - - - + + + + diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm index cc30638981..97e4771ff4 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm @@ -1151,16 +1151,15 @@ sub run_indb_circ_test { $self->circ_test_success($U->is_true($results->[0]->{success})); if(my $mp = $results->[0]->{matchpoint}) { - $logger->info("circulator: circ policy test found matchpoint $mp"); - $self->circ_matrix_matchpoint( - $self->editor->retrieve_config_circ_matrix_matchpoint([ - $mp, - { flesh => 1, - flesh_fields => {ccmm => - ['duration_rule', 'recurring_fine_rule', 'max_fine_rule', 'hard_due_date']} - } - ]) - ); + $logger->info("circulator: circ policy test found matchpoint built via rows " . $results->[0]->{buildrows}); + $self->circ_matrix_matchpoint($self->editor->retrieve_config_circ_matrix_matchpoint($mp)); + $self->circ_matrix_matchpoint->duration_rule($self->editor->retrieve_config_rules_circ_duration($results->[0]->{duration_rule})); + if($results->[0]->{renewals}) { + $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})); + $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})); } return $self->matrix_test_result($results); diff --git a/Open-ILS/src/sql/Pg/002.schema.config.sql b/Open-ILS/src/sql/Pg/002.schema.config.sql index 857d1a1fcd..839cf8bc32 100644 --- a/Open-ILS/src/sql/Pg/002.schema.config.sql +++ b/Open-ILS/src/sql/Pg/002.schema.config.sql @@ -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 ('0486'); -- berick +INSERT INTO config.upgrade_log (version) VALUES ('0487'); -- miker (for tsbere) CREATE TABLE config.bib_source ( id SERIAL PRIMARY KEY, diff --git a/Open-ILS/src/sql/Pg/100.circ_matrix.sql b/Open-ILS/src/sql/Pg/100.circ_matrix.sql index 0ed8e67962..e0f3b3dd62 100644 --- a/Open-ILS/src/sql/Pg/100.circ_matrix.sql +++ b/Open-ILS/src/sql/Pg/100.circ_matrix.sql @@ -95,11 +95,12 @@ CREATE TABLE config.circ_matrix_matchpoint ( usr_age_lower_bound INTERVAL, usr_age_upper_bound INTERVAL, -- "Result" Fields - circulate BOOL NOT NULL DEFAULT TRUE, -- Hard "can't circ" flag requiring an override - duration_rule INT NOT NULL REFERENCES config.rule_circ_duration (id) DEFERRABLE INITIALLY DEFERRED, - recurring_fine_rule INT NOT NULL REFERENCES config.rule_recurring_fine (id) DEFERRABLE INITIALLY DEFERRED, - max_fine_rule INT NOT NULL REFERENCES config.rule_max_fine (id) DEFERRABLE INITIALLY DEFERRED, + circulate BOOL, -- Hard "can't circ" flag requiring an override + duration_rule INT REFERENCES config.rule_circ_duration (id) DEFERRABLE INITIALLY DEFERRED, + recurring_fine_rule INT REFERENCES config.rule_recurring_fine (id) DEFERRABLE INITIALLY DEFERRED, + 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 script_test TEXT, -- javascript source total_copy_hold_ratio FLOAT, available_copy_hold_ratio FLOAT @@ -122,19 +123,24 @@ CREATE TABLE config.circ_matrix_circ_mod_test_map ( CONSTRAINT cm_once_per_test UNIQUE (circ_mod_test, circ_mod) ); -CREATE OR REPLACE FUNCTION action.find_circ_matrix_matchpoint( context_ou INT, match_item BIGINT, match_user INT, renewal BOOL ) RETURNS config.circ_matrix_matchpoint AS $func$ +CREATE TYPE action.found_circ_matrix_matchpoint AS ( success BOOL, matchpoint config.circ_matrix_matchpoint, buildrows INT[] ); + +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 - user_object actor.usr%ROWTYPE; - item_object asset.copy%ROWTYPE; 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 - SELECT INTO user_object * FROM actor.usr WHERE id = match_user; - SELECT INTO item_object * FROM asset.copy WHERE id = match_item; + -- 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; @@ -181,60 +187,126 @@ BEGIN ) SELECT INTO denominator MAX(distance) + 1 FROM all_distance; - -- Select the winning matchpoint into the matchpoint variable for returning - SELECT INTO matchpoint 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; - - -- Return the entire matchpoint - RETURN matchpoint; + -- 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; + 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; + +-- Helper function - For manual calling, it can be easier to pass in IDs instead of objects +CREATE OR REPLACE FUNCTION action.find_circ_matrix_matchpoint( context_ou INT, match_item BIGINT, match_user INT, renewal BOOL ) RETURNS SETOF action.found_circ_matrix_matchpoint AS $func$ +DECLARE + item_object asset.copy%ROWTYPE; + user_object actor.usr%ROWTYPE; +BEGIN + SELECT INTO item_object * FROM asset.copy WHERE id = match_item; + SELECT INTO user_object * FROM actor.usr WHERE id = match_user; + + RETURN QUERY SELECT * FROM action.find_circ_matrix_matchpoint( context_ou, item_object, user_object, renewal ); END; $func$ LANGUAGE plpgsql; @@ -291,25 +363,26 @@ BEGIN END; $func$ LANGUAGE PLPGSQL; -CREATE TYPE action.matrix_test_result AS ( success BOOL, matchpoint INT, fail_part TEXT ); -CREATE OR REPLACE FUNCTION action.item_user_circ_test( circ_ou INT, match_item BIGINT, match_user INT, renewal BOOL ) RETURNS SETOF action.matrix_test_result AS $func$ +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 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; + 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.matrix_test_result; - circ_test 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; - tmp_grp INT; - items_out INT; + 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; + done BOOL := FALSE; BEGIN + -- Assume success unless we hit a failure condition result.success := TRUE; -- Fail if the user is BARRED @@ -335,17 +408,6 @@ BEGIN RETURN; END IF; - SELECT INTO circ_test * FROM action.find_circ_matrix_matchpoint(circ_ou, match_item, match_user, renewal); - result.matchpoint := circ_test.id; - - -- Fail if we couldn't find a matchpoint - IF result.matchpoint IS NULL THEN - result.fail_part := 'no_matchpoint'; - result.success := FALSE; - done := TRUE; - RETURN NEXT result; - END IF; - IF user_object.barred IS TRUE THEN result.fail_part := 'actor.usr.barred'; result.success := FALSE; @@ -383,37 +445,29 @@ BEGIN RETURN NEXT result; END IF; - SELECT INTO context_org_list ARRAY_ACCUM(id) FROM actor.org_unit_full_path( circ_test.org_unit ); + SELECT INTO circ_test * FROM action.find_circ_matrix_matchpoint(circ_ou, item_object, user_object, renewal); - -- Fail if the test is set to hard non-circulating - IF circ_test.circulate IS FALSE THEN - result.fail_part := 'config.circ_matrix_test.circulate'; + 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.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; - -- Fail if the total copy-hold ratio is too low - IF circ_test.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_test.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_test.available_copy_hold_ratio IS NOT NULL THEN - SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item); - IF hold_ratio.available_copy_ratio IS NOT NULL AND hold_ratio.available_copy_ratio < circ_test.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; + -- 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%'; @@ -436,8 +490,40 @@ BEGIN 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_test.id LOOP + 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) @@ -463,11 +549,11 @@ BEGIN END; $func$ LANGUAGE plpgsql; -CREATE OR REPLACE FUNCTION action.item_user_circ_test( INT, BIGINT, INT ) RETURNS SETOF action.matrix_test_result AS $func$ +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.matrix_test_result AS $func$ +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; diff --git a/Open-ILS/src/sql/Pg/110.hold_matrix.sql b/Open-ILS/src/sql/Pg/110.hold_matrix.sql index 1821b1dc24..a0259791a6 100644 --- a/Open-ILS/src/sql/Pg/110.hold_matrix.sql +++ b/Open-ILS/src/sql/Pg/110.hold_matrix.sql @@ -200,6 +200,7 @@ BEGIN END; $func$ LANGUAGE 'plpgsql'; +CREATE TYPE action.matrix_test_result AS ( success BOOL, matchpoint INT, fail_part TEXT ); 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; diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql index 2e2ebbb8e9..409656b4bb 100644 --- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql +++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql @@ -2174,7 +2174,7 @@ SELECT SETVAL('asset.copy_location_id_seq'::TEXT, 100); 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,duration_rule,recurring_fine_rule,max_fine_rule) VALUES (1,1,11,1,1); +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_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, 8.0, 8.0, 8.0, 1.0, 6.0, 7.0, 0.0, 0.0), diff --git a/Open-ILS/src/sql/Pg/upgrade/0487.circ_matrix_fallthrough.sql b/Open-ILS/src/sql/Pg/upgrade/0487.circ_matrix_fallthrough.sql new file mode 100644 index 0000000000..0c4b71e84b --- /dev/null +++ b/Open-ILS/src/sql/Pg/upgrade/0487.circ_matrix_fallthrough.sql @@ -0,0 +1,404 @@ +BEGIN; + +INSERT INTO config.upgrade_log (version) VALUES ('0487'); -- tsbere via miker + +-- Circ matchpoint table changes + +ALTER TABLE config.circ_matrix_matchpoint + ALTER COLUMN circulate DROP NOT NULL, -- Fallthrough enable + ALTER COLUMN circulate DROP DEFAULT, -- Stop defaulting to true to enable default to fallthrough + ALTER COLUMN duration_rule DROP NOT NULL, -- Fallthrough enable + ALTER COLUMN recurring_fine_rule DROP NOT NULL, -- Fallthrough enable + ALTER COLUMN max_fine_rule DROP NOT NULL, -- Fallthrough enable + ADD COLUMN renewals INT; -- Renewals override + +-- Changing return types requires explicit dropping of old versions +DROP FUNCTION action.find_circ_matrix_matchpoint( context_ou INT, match_item BIGINT, match_user INT, renewal BOOL ); +DROP FUNCTION action.item_user_circ_test( circ_ou INT, match_item BIGINT, match_user INT, renewal BOOL ); +DROP FUNCTION action.item_user_circ_test( INT, BIGINT, INT ); +DROP FUNCTION action.item_user_renew_test( INT, BIGINT, INT ); + +-- New return types +CREATE TYPE action.found_circ_matrix_matchpoint AS ( success BOOL, matchpoint config.circ_matrix_matchpoint, buildrows 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 ); + +-- Replacement 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; + 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; + +-- Helper function - For manual calling, it can be easier to pass in IDs instead of objects +CREATE OR REPLACE FUNCTION action.find_circ_matrix_matchpoint( context_ou INT, match_item BIGINT, match_user INT, renewal BOOL ) RETURNS SETOF action.found_circ_matrix_matchpoint AS $func$ +DECLARE + item_object asset.copy%ROWTYPE; + user_object actor.usr%ROWTYPE; +BEGIN + SELECT INTO item_object * FROM asset.copy WHERE id = match_item; + SELECT INTO user_object * FROM actor.usr WHERE id = match_user; + + RETURN QUERY SELECT * FROM action.find_circ_matrix_matchpoint( context_ou, item_object, user_object, renewal ); +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.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; + +COMMIT; diff --git a/Open-ILS/web/js/dojo/openils/widget/AutoFieldWidget.js b/Open-ILS/web/js/dojo/openils/widget/AutoFieldWidget.js index ba05bc1b97..1db121a82d 100644 --- a/Open-ILS/web/js/dojo/openils/widget/AutoFieldWidget.js +++ b/Open-ILS/web/js/dojo/openils/widget/AutoFieldWidget.js @@ -142,6 +142,14 @@ if(!dojo._hasResource['openils.widget.AutoFieldWidget']) { */ getDisplayString : function() { var value = this.widgetValue; + if(this.inherits) { + switch(value) { + case null : + case undefined : + case 'unset' : + return openils.widget.AutoFieldWidget.localeStrings.INHERITED; + } + } switch(this.idlField.datatype) { case 'bool': switch(value) { @@ -272,13 +280,13 @@ if(!dojo._hasResource['openils.widget.AutoFieldWidget']) { break; case 'bool': - if(this.ternary) { + if(this.ternary || this.inherits) { dojo.require('dijit.form.FilteringSelect'); var store = new dojo.data.ItemFileReadStore({ data:{ identifier : 'value', items:[ - {label : openils.widget.AutoFieldWidget.localeStrings.UNSET, value : 'unset'}, + {label : (this.inherits ? openils.widget.AutoFieldWidget.localeStrings.INHERITED : openils.widget.AutoFieldWidget.localeStrings.UNSET), value : 'unset'}, {label : openils.widget.AutoFieldWidget.localeStrings.TRUE, value : 'true'}, {label : openils.widget.AutoFieldWidget.localeStrings.FALSE, value : 'false'} ] diff --git a/Open-ILS/web/js/dojo/openils/widget/AutoGrid.js b/Open-ILS/web/js/dojo/openils/widget/AutoGrid.js index 94f24bfa4b..3e99b2892f 100644 --- a/Open-ILS/web/js/dojo/openils/widget/AutoGrid.js +++ b/Open-ILS/web/js/dojo/openils/widget/AutoGrid.js @@ -611,15 +611,17 @@ if(!dojo._hasResource['openils.widget.AutoGrid']) { openils.widget.AutoGrid.defaultGetter = function(rowIndex, item) { if(!item) return ''; + if(!this.grid.overrideWidgetArgs[this.field]) + this.grid.overrideWidgetArgs[this.field] = {}; var val = this.grid.store.getValue(item, this.field); - var autoWidget = new openils.widget.AutoFieldWidget({ + var autoWidget = new openils.widget.AutoFieldWidget(dojo.mixin({ fmClass: this.grid.fmClass, fmField: this.field, widgetValue : val, readOnly : true, forceSync : true, // prevents many simultaneous requests for the same data suppressLinkedFields : this.grid.suppressLinkedFields - }); + },this.grid.overrideWidgetArgs[this.field])); autoWidget.build(); diff --git a/Open-ILS/web/js/dojo/openils/widget/EditPane.js b/Open-ILS/web/js/dojo/openils/widget/EditPane.js index e3262defc9..1fd41adb75 100644 --- a/Open-ILS/web/js/dojo/openils/widget/EditPane.js +++ b/Open-ILS/web/js/dojo/openils/widget/EditPane.js @@ -77,6 +77,20 @@ if(!dojo._hasResource['openils.widget.EditPane']) { if(field.name == this.fmIDL.pkey && this.mode == 'create' && this.fmIDL.pkey_sequence) continue; /* don't show auto-generated fields on create */ + if(!this.overrideWidgetArgs[field.name]) + this.overrideWidgetArgs[field.name] = {}; + + if(this.overrideWidgetArgs[field.name].hrbefore && this.paneStackCount <= 1) { + var hrTr = document.createElement('tr'); + var hrTd = document.createElement('td'); + var hr = document.createElement('hr'); + hrTd.colSpan = 2; + dojo.addClass(hrTd, 'openils-widget-editpane-hr-cell'); + hrTd.appendChild(hr); + hrTr.appendChild(hrTd); + tbody.appendChild(hrTr); + } + if((idx++ % this.paneStackCount) == 0 || !currentRow) { // time to start a new row currentRow = document.createElement('tr'); @@ -107,9 +121,6 @@ if(!dojo._hasResource['openils.widget.EditPane']) { currentRow.appendChild(valTd); //dojo.addClass(docTd, 'oils-fm-edit-pane-help'); - if(!this.overrideWidgetArgs[field.name]) - this.overrideWidgetArgs[field.name] = {}; - var args = dojo.mixin( { // defaults idlField : field, diff --git a/Open-ILS/web/js/dojo/openils/widget/nls/AutoFieldWidget.js b/Open-ILS/web/js/dojo/openils/widget/nls/AutoFieldWidget.js index 4d19f3419d..bc77c0689c 100644 --- a/Open-ILS/web/js/dojo/openils/widget/nls/AutoFieldWidget.js +++ b/Open-ILS/web/js/dojo/openils/widget/nls/AutoFieldWidget.js @@ -1,5 +1,6 @@ { "TRUE" : "True", "FALSE" : "False", - "UNSET" : "Unset" + "UNSET" : "Unset", + "INHERITED" : "Inherited" } diff --git a/Open-ILS/web/js/ui/default/conify/global/config/circ_matrix_matchpoint.js b/Open-ILS/web/js/ui/default/conify/global/config/circ_matrix_matchpoint.js index 7fdae7b166..f5d5b3a4b1 100644 --- a/Open-ILS/web/js/ui/default/conify/global/config/circ_matrix_matchpoint.js +++ b/Open-ILS/web/js/ui/default/conify/global/config/circ_matrix_matchpoint.js @@ -12,7 +12,18 @@ var circModEntryCache = {}; var matchPoint; function load(){ + cmGrid.overrideWidgetArgs.grp = {hrbefore : true}; cmGrid.overrideWidgetArgs.is_renewal = {ternary : true}; + cmGrid.overrideWidgetArgs.ref_flag = {ternary : true}; + cmGrid.overrideWidgetArgs.juvenile_flag = {ternary : true}; + cmGrid.overrideWidgetArgs.circulate = {inherits : true, hrbefore : true}; + cmGrid.overrideWidgetArgs.duration_rule = {inherits : true}; + cmGrid.overrideWidgetArgs.recurring_fine_rule = {inherits : true}; + cmGrid.overrideWidgetArgs.max_fine_rule = {inherits : true}; + cmGrid.overrideWidgetArgs.available_copy_hold_ratio = {inherits : true}; + cmGrid.overrideWidgetArgs.total_copy_hold_ratio = {inherits : true}; + cmGrid.overrideWidgetArgs.renewals = {inherits : true}; + cmGrid.overrideWidgetArgs.hard_due_date = {inherits : true}; cmGrid.loadAll({order_by:{ccmm:'circ_modifier'}}); cmGrid.onEditPane = buildEditPaneAdditions; circModEditor = dojo.byId('circ-mod-editor').parentNode.removeChild(dojo.byId('circ-mod-editor')); diff --git a/Open-ILS/web/templates/default/conify/global/config/circ_matrix_matchpoint.tt2 b/Open-ILS/web/templates/default/conify/global/config/circ_matrix_matchpoint.tt2 index 6054078087..c09012fbe4 100644 --- a/Open-ILS/web/templates/default/conify/global/config/circ_matrix_matchpoint.tt2 +++ b/Open-ILS/web/templates/default/conify/global/config/circ_matrix_matchpoint.tt2 @@ -9,7 +9,7 @@
[% END %] -- 2.43.2