Index: Open-ILS/src/sql/Pg/099.matrix_weights.sql =================================================================== --- Open-ILS/src/sql/Pg/099.matrix_weights.sql (revision 0) +++ Open-ILS/src/sql/Pg/099.matrix_weights.sql (revision 0) @@ -0,0 +1,53 @@ + +BEGIN; + +-- Circ Matrix Weights +CREATE TABLE config.circ_matrix_weights ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + org_unit NUMERIC(6,2) NOT NULL, + grp NUMERIC(6,2) NOT NULL, + circ_modifier NUMERIC(6,2) NOT NULL, + marc_type NUMERIC(6,2) NOT NULL, + marc_form NUMERIC(6,2) NOT NULL, + marc_vr_format NUMERIC(6,2) NOT NULL, + copy_circ_lib NUMERIC(6,2) NOT NULL, + copy_owning_lib NUMERIC(6,2) NOT NULL, + user_home_ou NUMERIC(6,2) NOT NULL, + ref_flag NUMERIC(6,2) NOT NULL, + juvenile_flag NUMERIC(6,2) NOT NULL, + is_renewal NUMERIC(6,2) NOT NULL, + usr_age_lower_bound NUMERIC(6,2) NOT NULL, + usr_age_upper_bound NUMERIC(6,2) NOT NULL +); + +-- Hold Matrix Weights +CREATE TABLE config.hold_matrix_weights ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + user_home_ou NUMERIC(6,2) NOT NULL, + request_ou NUMERIC(6,2) NOT NULL, + pickup_ou NUMERIC(6,2) NOT NULL, + item_owning_ou NUMERIC(6,2) NOT NULL, + item_circ_ou NUMERIC(6,2) NOT NULL, + usr_grp NUMERIC(6,2) NOT NULL, + requestor_grp NUMERIC(6,2) NOT NULL, + circ_modifier NUMERIC(6,2) NOT NULL, + marc_type NUMERIC(6,2) NOT NULL, + marc_form NUMERIC(6,2) NOT NULL, + marc_vr_format NUMERIC(6,2) NOT NULL, + juvenile_flag NUMERIC(6,2) NOT NULL, + ref_flag NUMERIC(6,2) NOT NULL +); + +-- Linking between weights and org units +CREATE TABLE config.weight_assoc ( + id SERIAL PRIMARY KEY, + active BOOL NOT NULL, + org_unit INT NOT NULL REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + circ_weights INT REFERENCES config.circ_matrix_weights (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED, + hold_weights INT REFERENCES config.hold_matrix_weights (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED +); +CREATE UNIQUE INDEX once_per_ou ON config.weight_assoc (org_unit) WHERE active; + +COMMIT; Index: Open-ILS/src/sql/Pg/upgrade/0XXX.config.matrix_weights.sql =================================================================== --- Open-ILS/src/sql/Pg/upgrade/0XXX.config.matrix_weights.sql (revision 0) +++ Open-ILS/src/sql/Pg/upgrade/0XXX.config.matrix_weights.sql (revision 0) @@ -0,0 +1,370 @@ +BEGIN; + +INSERT INTO config.upgrade_log (version) VALUES ('0XXX'); + +CREATE OR REPLACE FUNCTION permission.grp_ancestors_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$ + WITH RECURSIVE grp_ancestors_distance(id, distance) AS ( + SELECT $1, 0 + UNION + SELECT pgt.parent, gad.distance+1 + FROM permission.grp_tree pgt JOIN grp_ancestors_distance gad ON pgt.id = gad.id + WHERE pgt.parent IS NOT NULL + ) + SELECT * FROM grp_ancestors_distance; +$$ LANGUAGE SQL STABLE; + +CREATE OR REPLACE FUNCTION permission.grp_descendants_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$ + WITH RECURSIVE grp_descendants_distance(id, distance) AS ( + SELECT $1, 0 + UNION + SELECT pgt.id, gdd.distance+1 + FROM permission.grp_tree pgt JOIN grp_descendants_distance gdd ON pgt.parent = gdd.id + ) + SELECT * FROM grp_descendants_distance; +$$ LANGUAGE SQL STABLE; + +CREATE OR REPLACE FUNCTION actor.org_unit_ancestors_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$ + WITH RECURSIVE org_unit_ancestors_distance(id, distance) AS ( + SELECT $1, 0 + UNION + SELECT ou.parent_ou, ouad.distance+1 + FROM actor.org_unit ou JOIN org_unit_ancestors_distance ouad ON ou.id = ouad.id + WHERE ou.parent_ou IS NOT NULL + ) + SELECT * FROM org_unit_ancestors_distance; +$$ LANGUAGE SQL STABLE; + +CREATE OR REPLACE FUNCTION actor.org_unit_descendants_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$ + WITH RECURSIVE org_unit_descendants_distance(id, distance) AS ( + SELECT $1, 0 + UNION + SELECT ou.id, oudd.distance+1 + FROM actor.org_unit ou JOIN org_unit_descendants_distance oudd ON ou.parent_ou = oudd.id + ) + SELECT * FROM org_unit_descendants_distance; +$$ LANGUAGE SQL STABLE; + +ALTER TABLE config.circ_matrix_matchpoint + ADD COLUMN user_home_ou INT REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED; + +CREATE TABLE config.circ_matrix_weights ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + org_unit NUMERIC(6,2) NOT NULL, + grp NUMERIC(6,2) NOT NULL, + circ_modifier NUMERIC(6,2) NOT NULL, + marc_type NUMERIC(6,2) NOT NULL, + marc_form NUMERIC(6,2) NOT NULL, + marc_vr_format NUMERIC(6,2) NOT NULL, + copy_circ_lib NUMERIC(6,2) NOT NULL, + copy_owning_lib NUMERIC(6,2) NOT NULL, + user_home_ou NUMERIC(6,2) NOT NULL, + ref_flag NUMERIC(6,2) NOT NULL, + juvenile_flag NUMERIC(6,2) NOT NULL, + is_renewal NUMERIC(6,2) NOT NULL, + usr_age_lower_bound NUMERIC(6,2) NOT NULL, + usr_age_upper_bound NUMERIC(6,2) NOT NULL +); + +CREATE TABLE config.hold_matrix_weights ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + user_home_ou NUMERIC(6,2) NOT NULL, + request_ou NUMERIC(6,2) NOT NULL, + pickup_ou NUMERIC(6,2) NOT NULL, + item_owning_ou NUMERIC(6,2) NOT NULL, + item_circ_ou NUMERIC(6,2) NOT NULL, + usr_grp NUMERIC(6,2) NOT NULL, + requestor_grp NUMERIC(6,2) NOT NULL, + circ_modifier NUMERIC(6,2) NOT NULL, + marc_type NUMERIC(6,2) NOT NULL, + marc_form NUMERIC(6,2) NOT NULL, + marc_vr_format NUMERIC(6,2) NOT NULL, + juvenile_flag NUMERIC(6,2) NOT NULL, + ref_flag NUMERIC(6,2) NOT NULL +); + +CREATE TABLE config.weight_assoc ( + id SERIAL PRIMARY KEY, + active BOOL NOT NULL, + org_unit INT NOT NULL REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, + circ_weights INT REFERENCES config.circ_matrix_weights (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED, + hold_weights INT REFERENCES config.hold_matrix_weights (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED +); +CREATE UNIQUE INDEX once_per_ou ON config.weight_assoc (org_unit) WHERE active; + +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$ +DECLARE + user_object actor.usr%ROWTYPE; + item_object asset.copy%ROWTYPE; + cn_object asset.call_number%ROWTYPE; + rec_descriptor metabib.rec_descriptor%ROWTYPE; + matchpoint config.circ_matrix_matchpoint%ROWTYPE; + weights config.circ_matrix_weights%ROWTYPE; + user_age INTERVAL; + denominator INT; +BEGIN + SELECT INTO user_object * FROM actor.usr WHERE id = match_user; + SELECT INTO item_object * FROM asset.copy WHERE id = match_item; + 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. + -- If we get a null set a default weighting will be pulled via COALESCE in the query. + 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; + weights.org_unit := 10; + weights.circ_modifier := 5; + weights.marc_type := 4; + weights.marc_form := 3; + weights.marc_vr_format := 2; + weights.copy_circ_lib := 8; + weights.copy_owning_lib := 8; + weights.user_home_ou := 8; + weights.ref_flag := 1; + weights.juvenile_flag := 6; + weights.is_renewal := 7; + weights.usr_age_lower_bound := 0; + weights.usr_age_upper_bound := 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; + + -- 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 END + + -- Org Units + CASE WHEN ctoua.distance IS NOT NULL THEN 2^(2*weights.org_unit - (ctoua.distance/denominator)) ELSE 0 END + + CASE WHEN cnoua.distance IS NOT NULL THEN 2^(2*weights.copy_owning_lib - (cnoua.distance/denominator)) ELSE 0 END + + CASE WHEN iooua.distance IS NOT NULL THEN 2^(2*weights.copy_circ_lib - (iooua.distance/denominator)) ELSE 0 END + + CASE WHEN uhoua.distance IS NOT NULL THEN 2^(2*weights.user_home_ou - (uhoua.distance/denominator)) ELSE 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 END + + -- Static User Checks + CASE WHEN m.juvenile_flag IS NOT NULL THEN 4^weights.juvenile_flag ELSE 0 END + + CASE WHEN m.usr_age_lower_bound IS NOT NULL THEN 4^weights.usr_age_lower_bound ELSE 0 END + + CASE WHEN m.usr_age_upper_bound IS NOT NULL THEN 4^weights.usr_age_upper_bound ELSE 0 END + + -- Static Item Checks + CASE WHEN m.circ_modifier IS NOT NULL THEN 4^weights.circ_modifier ELSE 0 END + + CASE WHEN m.marc_type IS NOT NULL THEN 4^weights.marc_type ELSE 0 END + + CASE WHEN m.marc_form IS NOT NULL THEN 4^weights.marc_form ELSE 0 END + + CASE WHEN m.marc_vr_format IS NOT NULL THEN 4^weights.marc_vr_format ELSE 0 END + + CASE WHEN m.ref_flag IS NOT NULL THEN 4^weights.ref_flag ELSE 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; +END; +$func$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION action.find_hold_matrix_matchpoint(pickup_ou integer, request_ou integer, match_item bigint, match_user integer, match_requestor integer) + RETURNS integer AS +$func$ +DECLARE + requestor_object actor.usr%ROWTYPE; + user_object actor.usr%ROWTYPE; + item_object asset.copy%ROWTYPE; + item_cn_object asset.call_number%ROWTYPE; + rec_descriptor metabib.rec_descriptor%ROWTYPE; + matchpoint config.hold_matrix_matchpoint%ROWTYPE; + weights config.hold_matrix_weights%ROWTYPE; + denominator INT; +BEGIN + SELECT INTO user_object * FROM actor.usr WHERE id = match_user; + SELECT INTO requestor_object * FROM actor.usr WHERE id = match_requestor; + SELECT INTO item_object * FROM asset.copy WHERE id = match_item; + SELECT INTO item_cn_object * FROM asset.call_number WHERE id = item_object.call_number; + SELECT INTO rec_descriptor * FROM metabib.rec_descriptor WHERE record = item_cn_object.record; + + -- The item's owner should probably be the one determining if the item is holdable + -- How to decide that is debatable. Decided to default to the circ library (where the item lives) + -- This flag will allow for setting it to the owning library (where the call number "lives") + PERFORM * FROM config.internal_flag WHERE name = 'circ.holds.weight_owner_not_circ' AND enabled; + + -- Grab the closest set circ weight setting. + -- If we get a null set a default weighting will be pulled via COALESCE in the query. + IF NOT FOUND THEN + -- Default to circ library + SELECT INTO weights hw.* + FROM config.weight_assoc wa + JOIN config.hold_matrix_weights hw ON (hw.id = wa.hold_weights) + JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) d ON (wa.org_unit = d.id) + WHERE active + ORDER BY d.distance + LIMIT 1; + ELSE + -- Flag is set, use owning library + SELECT INTO weights hw.* + FROM config.weight_assoc wa + JOIN config.hold_matrix_weights hw ON (hw.id = wa.hold_weights) + JOIN actor.org_unit_ancestors_distance( cn_object.owning_lib ) d ON (wa.org_unit = d.id) + WHERE active + ORDER BY d.distance + LIMIT 1; + END IF; + + -- No weights? Bad admin! Defaults to handle that anyway. + IF weights.id IS NULL THEN + weights.user_home_ou := 5; + weights.request_ou := 5; + weights.pickup_ou := 5; + weights.item_owning_ou := 5; + weights.item_circ_ou := 5; + weights.usr_grp := 7; + weights.requestor_grp := 8; + weights.circ_modifier := 4; + weights.marc_type := 3; + weights.marc_form := 2; + weights.marc_vr_format := 1; + weights.juvenile_flag := 4; + weights.ref_flag := 0; + END IF; + + -- Determine the max (expected) depth (+1) of the org tree and max depth of the permisson tree + -- If you break your org tree with funky parenting this may be wrong + -- Note: This CTE is duplicated in the find_circ_matrix_matchpoint function, and it may be a good idea to split it off to a function + -- We use one denominator for all tree-based checks for when permission groups and org units have the same weighting + WITH all_distance(distance) AS ( + SELECT depth AS distance FROM actor.org_unit_type + UNION + SELECT distance AS distance FROM permission.grp_ancestors_distance((SELECT id FROM permission.grp_tree WHERE parent IS NULL)) + ) + SELECT INTO denominator MAX(distance) + 1 FROM all_distance; + + -- To ATTEMPT to make this work like it used to, make it reverse the user/requestor profile ids. + -- This may be better implemented as part of the upgrade script? + -- Set usr_grp = requestor_grp, requestor_grp = 1 or something when this flag is already set + -- Then remove this flag, of course. + PERFORM * FROM config.internal_flag WHERE name = 'circ.holds.usr_not_requestor' AND enabled; + + IF FOUND THEN + -- Note: This, to me, is REALLY hacky. I put it in anyway. + -- If you can't tell, this is a single call swap on two variables. + SELECT INTO user_object.profile, requestor_object.profile + requestor_object.profile, user_object.profile; + END IF; + + -- Select the winning matchpoint into the matchpoint variable for returning + SELECT INTO matchpoint m.* + FROM config.hold_matrix_matchpoint m + /*LEFT*/ JOIN permission.grp_ancestors_distance( requestor_object.profile ) rpgad ON m.requestor_grp = rpgad.id + LEFT JOIN permission.grp_ancestors_distance( user_object.profile ) upgad ON m.usr_grp = upgad.id + LEFT JOIN actor.org_unit_ancestors_distance( pickup_ou ) puoua ON m.pickup_ou = puoua.id + LEFT JOIN actor.org_unit_ancestors_distance( request_ou ) rqoua ON m.request_ou = rqoua.id + LEFT JOIN actor.org_unit_ancestors_distance( item_cn_object.owning_lib ) cnoua ON m.item_owning_ou = cnoua.id + LEFT JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) iooua ON m.item_circ_ou = iooua.id + LEFT JOIN actor.org_unit_ancestors_distance( user_object.home_ou ) uhoua ON m.user_home_ou = uhoua.id + WHERE m.active + -- Permission Groups + -- AND (m.requestor_grp IS NULL OR upgad.id IS NOT NULL) -- Optional Requestor Group? + AND (m.usr_grp IS NULL OR upgad.id IS NOT NULL) + -- Org Units + AND (m.pickup_ou IS NULL OR (puoua.id IS NOT NULL AND (puoua.distance = 0 OR NOT m.strict_ou_match))) + AND (m.request_ou IS NULL OR (rqoua.id IS NOT NULL AND (rqoua.distance = 0 OR NOT m.strict_ou_match))) + AND (m.item_owning_ou IS NULL OR (cnoua.id IS NOT NULL AND (cnoua.distance = 0 OR NOT m.strict_ou_match))) + AND (m.item_circ_ou IS NULL OR (iooua.id IS NOT NULL AND (iooua.distance = 0 OR NOT m.strict_ou_match))) + AND (m.user_home_ou IS NULL OR (uhoua.id IS NOT NULL AND (uhoua.distance = 0 OR NOT m.strict_ou_match))) + -- Static User Checks + AND (m.juvenile_flag IS NULL OR m.juvenile_flag = user_object.juvenile) + -- Static Item Checks + AND (m.circ_modifier IS NULL OR m.circ_modifier = item_object.circ_modifier) + AND (m.marc_type IS NULL OR m.marc_type = COALESCE(item_object.circ_as_type, rec_descriptor.item_type)) + AND (m.marc_form IS NULL OR m.marc_form = rec_descriptor.item_form) + AND (m.marc_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 rpgad.distance IS NOT NULL THEN 2^(2*weights.requestor_grp - (rpgad.distance/denominator)) ELSE 0 END + + CASE WHEN upgad.distance IS NOT NULL THEN 2^(2*weights.usr_grp - (upgad.distance/denominator)) ELSE 0 END + + -- Org Units + CASE WHEN puoua.distance IS NOT NULL THEN 2^(2*weights.pickup_ou - (puoua.distance/denominator)) ELSE 0 END + + CASE WHEN rqoua.distance IS NOT NULL THEN 2^(2*weights.request_ou - (rqoua.distance/denominator)) ELSE 0 END + + CASE WHEN cnoua.distance IS NOT NULL THEN 2^(2*weights.item_owning_ou - (cnoua.distance/denominator)) ELSE 0 END + + CASE WHEN iooua.distance IS NOT NULL THEN 2^(2*weights.item_circ_ou - (iooua.distance/denominator)) ELSE 0 END + + CASE WHEN uhoua.distance IS NOT NULL THEN 2^(2*weights.user_home_ou - (uhoua.distance/denominator)) ELSE 0 END + + -- Static User Checks -- Note: 4^x is equiv to 2^(2*x) + CASE WHEN m.juvenile_flag IS NOT NULL THEN 4^weights.juvenile_flag ELSE 0 END + + -- Static Item Checks + CASE WHEN m.circ_modifier IS NOT NULL THEN 4^weights.circ_modifier ELSE 0 END + + CASE WHEN m.marc_type IS NOT NULL THEN 4^weights.marc_type ELSE 0 END + + CASE WHEN m.marc_form IS NOT NULL THEN 4^weights.marc_form ELSE 0 END + + CASE WHEN m.marc_vr_format IS NOT NULL THEN 4^weights.marc_vr_format ELSE 0 END + + CASE WHEN m.ref_flag IS NOT NULL THEN 4^weights.ref_flag ELSE 0 END DESC, + -- Final sort on id, so that if two rules have the same sorting in the previous sort they have a defined order + -- This prevents "we changed the table order by updating a rule, and we started getting different results" + m.id; + + -- Return just the ID for now + RETURN matchpoint.id; +END; +$func$ LANGUAGE 'plpgsql'; + +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), + ('Org_Unit_First', 11.0, 10.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), + ('Item_Owner_First', 8.0, 8.0, 5.0, 4.0, 3.0, 2.0, 10.0, 11.0, 8.0, 1.0, 6.0, 7.0, 0.0, 0.0), + ('All_Equal', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0); + +INSERT INTO config.hold_matrix_weights(name, user_home_ou, request_ou, pickup_ou, item_owning_ou, item_circ_ou, usr_grp, requestor_grp, circ_modifier, marc_type, marc_form, marc_vr_format, juvenile_flag, ref_flag) VALUES + ('Default', 5.0, 5.0, 5.0, 5.0, 5.0, 7.0, 8.0, 4.0, 3.0, 2.0, 1.0, 4.0, 0.0), + ('Item_Owner_First', 5.0, 5.0, 5.0, 8.0, 7.0, 5.0, 5.0, 4.0, 3.0, 2.0, 1.0, 4.0, 0.0), + ('User_Before_Requestor', 5.0, 5.0, 5.0, 5.0, 5.0, 8.0, 7.0, 4.0, 3.0, 2.0, 1.0, 4.0, 0.0), + ('All_Equal', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0); + +INSERT INTO config.weight_assoc(active, org_unit, circ_weights, hold_weights) VALUES + (true, 1, 1, 1); + +COMMIT; Index: Open-ILS/src/sql/Pg/build-db.sh =================================================================== --- Open-ILS/src/sql/Pg/build-db.sh (revision 19247) +++ Open-ILS/src/sql/Pg/build-db.sh (working copy) @@ -101,7 +101,8 @@ 080.schema.money.sql 090.schema.action.sql 095.schema.booking.sql - + + 099.matrix_weights.sql 100.circ_matrix.sql 110.hold_matrix.sql Index: Open-ILS/src/sql/Pg/950.data.seed-values.sql =================================================================== --- Open-ILS/src/sql/Pg/950.data.seed-values.sql (revision 19247) +++ Open-ILS/src/sql/Pg/950.data.seed-values.sql (working copy) @@ -1621,11 +1621,25 @@ -- 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_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), + ('Org_Unit_First', 11.0, 10.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), + ('Item_Owner_First', 8.0, 8.0, 5.0, 4.0, 3.0, 2.0, 10.0, 11.0, 8.0, 1.0, 6.0, 7.0, 0.0, 0.0), + ('All_Equal', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0); -- hold matrix - 110.hold_matrix.sql: INSERT INTO config.hold_matrix_matchpoint (requestor_grp) VALUES (1); +INSERT INTO config.hold_matrix_weights(name, user_home_ou, request_ou, pickup_ou, item_owning_ou, item_circ_ou, usr_grp, requestor_grp, circ_modifier, marc_type, marc_form, marc_vr_format, juvenile_flag, ref_flag) VALUES + ('Default', 5.0, 5.0, 5.0, 5.0, 5.0, 7.0, 8.0, 4.0, 3.0, 2.0, 1.0, 4.0, 0.0), + ('Item_Owner_First', 5.0, 5.0, 5.0, 8.0, 7.0, 5.0, 5.0, 4.0, 3.0, 2.0, 1.0, 4.0, 0.0), + ('User_Before_Requestor', 5.0, 5.0, 5.0, 5.0, 5.0, 8.0, 7.0, 4.0, 3.0, 2.0, 1.0, 4.0, 0.0), + ('All_Equal', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0); +-- dynamic weight associations +INSERT INTO config.weight_assoc(active, org_unit, circ_weights, hold_weights) VALUES + (true, 1, 1, 1); + -- User setting types INSERT INTO config.usr_setting_type (name,opac_visible,label,description,datatype) VALUES ('opac.default_font', TRUE, 'OPAC Font Size', 'OPAC Font Size', 'string'); Index: Open-ILS/src/sql/Pg/100.circ_matrix.sql =================================================================== --- Open-ILS/src/sql/Pg/100.circ_matrix.sql (revision 19247) +++ Open-ILS/src/sql/Pg/100.circ_matrix.sql (working copy) @@ -76,20 +76,6 @@ ** developers focus on specific parts of the matrix. **/ - --- --- ****** Which ruleset and tests to use ******* --- --- * Most specific range for org_unit and grp wins. --- --- * circ_modifier match takes precidence over marc_type match, if circ_modifier is set here --- --- * marc_type is first checked against the circ_as_type from the copy, then the item type from the marc record --- --- * If neither circ_modifier nor marc_type is set (both are NULLABLE) then the entry defines the default --- ruleset and tests for the OU + group (like BOOK in PINES) --- - CREATE TABLE config.circ_matrix_matchpoint ( id SERIAL PRIMARY KEY, active BOOL NOT NULL DEFAULT TRUE, @@ -101,6 +87,7 @@ marc_vr_format TEXT REFERENCES config.videorecording_format_map (code) DEFERRABLE INITIALLY DEFERRED, copy_circ_lib INT REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED, copy_owning_lib INT REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED, + user_home_ou INT REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED, ref_flag BOOL, juvenile_flag BOOL, is_renewal BOOL, @@ -138,102 +125,117 @@ 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$ DECLARE - current_group permission.grp_tree%ROWTYPE; - user_object actor.usr%ROWTYPE; - item_object asset.copy%ROWTYPE; - cn_object asset.call_number%ROWTYPE; - rec_descriptor metabib.rec_descriptor%ROWTYPE; - current_mp config.circ_matrix_matchpoint%ROWTYPE; - matchpoint config.circ_matrix_matchpoint%ROWTYPE; + user_object actor.usr%ROWTYPE; + item_object asset.copy%ROWTYPE; + cn_object asset.call_number%ROWTYPE; + rec_descriptor metabib.rec_descriptor%ROWTYPE; + matchpoint config.circ_matrix_matchpoint%ROWTYPE; + weights config.circ_matrix_weights%ROWTYPE; + user_age INTERVAL; + denominator INT; BEGIN - SELECT INTO user_object * FROM actor.usr WHERE id = match_user; - SELECT INTO item_object * FROM asset.copy WHERE id = match_item; - SELECT INTO cn_object * FROM asset.call_number WHERE id = item_object.call_number; - SELECT INTO rec_descriptor r.* FROM metabib.rec_descriptor r JOIN asset.call_number c USING (record) WHERE c.id = item_object.call_number; - SELECT INTO current_group * FROM permission.grp_tree WHERE id = user_object.profile; + SELECT INTO user_object * FROM actor.usr WHERE id = match_user; + SELECT INTO item_object * FROM asset.copy WHERE id = match_item; + 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; - LOOP - -- for each potential matchpoint for this ou and group ... - FOR current_mp IN - SELECT m.* - FROM config.circ_matrix_matchpoint m - JOIN actor.org_unit_ancestors( context_ou ) d ON (m.org_unit = d.id) - LEFT JOIN actor.org_unit_proximity p ON (p.from_org = context_ou AND p.to_org = d.id) - WHERE m.grp = current_group.id - AND m.active - AND (m.copy_owning_lib IS NULL OR cn_object.owning_lib IN ( SELECT id FROM actor.org_unit_descendants(m.copy_owning_lib) )) - AND (m.copy_circ_lib IS NULL OR item_object.circ_lib IN ( SELECT id FROM actor.org_unit_descendants(m.copy_circ_lib) )) - ORDER BY CASE WHEN p.prox IS NULL THEN 999 ELSE p.prox END, - CASE WHEN m.copy_owning_lib IS NOT NULL - THEN 256 / ( SELECT COALESCE(prox, 255) + 1 FROM actor.org_unit_proximity WHERE to_org = cn_object.owning_lib AND from_org = m.copy_owning_lib LIMIT 1 ) - ELSE 0 - END + - CASE WHEN m.copy_circ_lib IS NOT NULL - THEN 256 / ( SELECT COALESCE(prox, 255) + 1 FROM actor.org_unit_proximity WHERE to_org = item_object.circ_lib AND from_org = m.copy_circ_lib LIMIT 1 ) - ELSE 0 - END + - CASE WHEN m.is_renewal = renewal THEN 128 ELSE 0 END + - CASE WHEN m.juvenile_flag IS NOT NULL THEN 64 ELSE 0 END + - CASE WHEN m.circ_modifier IS NOT NULL THEN 32 ELSE 0 END + - CASE WHEN m.marc_type IS NOT NULL THEN 16 ELSE 0 END + - CASE WHEN m.marc_form IS NOT NULL THEN 8 ELSE 0 END + - CASE WHEN m.marc_vr_format IS NOT NULL THEN 4 ELSE 0 END + - CASE WHEN m.ref_flag IS NOT NULL THEN 2 ELSE 0 END + - CASE WHEN m.usr_age_lower_bound IS NOT NULL THEN 0.5 ELSE 0 END + - CASE WHEN m.usr_age_upper_bound IS NOT NULL THEN 0.5 ELSE 0 END DESC LOOP + -- 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; - IF current_mp.is_renewal IS NOT NULL THEN - CONTINUE WHEN current_mp.is_renewal <> renewal; - END IF; + -- Grab the closest set circ weight setting. + -- If we get a null set a default weighting will be pulled via COALESCE in the query. + 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; - IF current_mp.circ_modifier IS NOT NULL THEN - CONTINUE WHEN current_mp.circ_modifier <> item_object.circ_modifier OR item_object.circ_modifier IS NULL; - END IF; + -- No weights? Bad admin! Defaults to handle that anyway. + IF weights.id IS NULL THEN + weights.grp := 11; + weights.org_unit := 10; + weights.circ_modifier := 5; + weights.marc_type := 4; + weights.marc_form := 3; + weights.marc_vr_format := 2; + weights.copy_circ_lib := 8; + weights.copy_owning_lib := 8; + weights.user_home_ou := 8; + weights.ref_flag := 1; + weights.juvenile_flag := 6; + weights.is_renewal := 7; + weights.usr_age_lower_bound := 0; + weights.usr_age_upper_bound := 0; + END IF; - IF current_mp.marc_type IS NOT NULL THEN - IF item_object.circ_as_type IS NOT NULL THEN - CONTINUE WHEN current_mp.marc_type <> item_object.circ_as_type; - ELSE - CONTINUE WHEN current_mp.marc_type <> rec_descriptor.item_type; - END IF; - 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; - IF current_mp.marc_form IS NOT NULL THEN - CONTINUE WHEN current_mp.marc_form <> rec_descriptor.item_form; - END IF; + -- 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 END + + -- Org Units + CASE WHEN ctoua.distance IS NOT NULL THEN 2^(2*weights.org_unit - (ctoua.distance/denominator)) ELSE 0 END + + CASE WHEN cnoua.distance IS NOT NULL THEN 2^(2*weights.copy_owning_lib - (cnoua.distance/denominator)) ELSE 0 END + + CASE WHEN iooua.distance IS NOT NULL THEN 2^(2*weights.copy_circ_lib - (iooua.distance/denominator)) ELSE 0 END + + CASE WHEN uhoua.distance IS NOT NULL THEN 2^(2*weights.user_home_ou - (uhoua.distance/denominator)) ELSE 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 END + + -- Static User Checks + CASE WHEN m.juvenile_flag IS NOT NULL THEN 4^weights.juvenile_flag ELSE 0 END + + CASE WHEN m.usr_age_lower_bound IS NOT NULL THEN 4^weights.usr_age_lower_bound ELSE 0 END + + CASE WHEN m.usr_age_upper_bound IS NOT NULL THEN 4^weights.usr_age_upper_bound ELSE 0 END + + -- Static Item Checks + CASE WHEN m.circ_modifier IS NOT NULL THEN 4^weights.circ_modifier ELSE 0 END + + CASE WHEN m.marc_type IS NOT NULL THEN 4^weights.marc_type ELSE 0 END + + CASE WHEN m.marc_form IS NOT NULL THEN 4^weights.marc_form ELSE 0 END + + CASE WHEN m.marc_vr_format IS NOT NULL THEN 4^weights.marc_vr_format ELSE 0 END + + CASE WHEN m.ref_flag IS NOT NULL THEN 4^weights.ref_flag ELSE 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; - IF current_mp.marc_vr_format IS NOT NULL THEN - CONTINUE WHEN current_mp.marc_vr_format <> rec_descriptor.vr_format; - END IF; - - IF current_mp.ref_flag IS NOT NULL THEN - CONTINUE WHEN current_mp.ref_flag <> item_object.ref; - END IF; - - IF current_mp.juvenile_flag IS NOT NULL THEN - CONTINUE WHEN current_mp.juvenile_flag <> user_object.juvenile; - END IF; - - IF current_mp.usr_age_lower_bound IS NOT NULL THEN - CONTINUE WHEN user_object.dob IS NULL OR current_mp.usr_age_lower_bound < age(user_object.dob); - END IF; - - IF current_mp.usr_age_upper_bound IS NOT NULL THEN - CONTINUE WHEN user_object.dob IS NULL OR current_mp.usr_age_upper_bound > age(user_object.dob); - END IF; - - - -- everything was undefined or matched - matchpoint = current_mp; - - EXIT WHEN matchpoint.id IS NOT NULL; - END LOOP; - - EXIT WHEN current_group.parent IS NULL OR matchpoint.id IS NOT NULL; - - SELECT INTO current_group * FROM permission.grp_tree WHERE id = current_group.parent; - END LOOP; - + -- Return the entire matchpoint RETURN matchpoint; END; $func$ LANGUAGE plpgsql; Index: Open-ILS/src/sql/Pg/020.schema.functions.sql =================================================================== --- Open-ILS/src/sql/Pg/020.schema.functions.sql (revision 19247) +++ Open-ILS/src/sql/Pg/020.schema.functions.sql (working copy) @@ -224,6 +224,16 @@ ) SELECT ou.* FROM actor.org_unit ou JOIN descendant_depth USING (id); $$ LANGUAGE SQL; +CREATE OR REPLACE FUNCTION actor.org_unit_descendants_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$ + WITH RECURSIVE org_unit_descendants_distance(id, distance) AS ( + SELECT $1, 0 + UNION + SELECT ou.id, oudd.distance+1 + FROM actor.org_unit ou JOIN org_unit_descendants_distance oudd ON ou.parent_ou = oudd.id + ) + SELECT * FROM org_unit_descendants_distance; +$$ LANGUAGE SQL STABLE; + CREATE OR REPLACE FUNCTION actor.org_unit_ancestors( INT ) RETURNS SETOF actor.org_unit AS $$ WITH RECURSIVE anscestor_depth AS ( SELECT ou.id, @@ -247,6 +257,17 @@ ON x.ou_type = y.id AND y.depth = $2); $$ LANGUAGE SQL STABLE; +CREATE OR REPLACE FUNCTION actor.org_unit_ancestors_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$ + WITH RECURSIVE org_unit_ancestors_distance(id, distance) AS ( + SELECT $1, 0 + UNION + SELECT ou.parent_ou, ouad.distance+1 + FROM actor.org_unit ou JOIN org_unit_ancestors_distance ouad ON ou.id = ouad.id + WHERE ou.parent_ou IS NOT NULL + ) + SELECT * FROM org_unit_ancestors_distance; +$$ LANGUAGE SQL STABLE; + CREATE OR REPLACE FUNCTION actor.org_unit_full_path ( INT ) RETURNS SETOF actor.org_unit AS $$ SELECT * FROM actor.org_unit_ancestors($1) Index: Open-ILS/src/sql/Pg/110.hold_matrix.sql =================================================================== --- Open-ILS/src/sql/Pg/110.hold_matrix.sql (revision 19247) +++ Open-ILS/src/sql/Pg/110.hold_matrix.sql (working copy) @@ -55,156 +55,148 @@ CONSTRAINT hous_once_per_grp_loc_mod_marc UNIQUE (user_home_ou, request_ou, pickup_ou, item_owning_ou, item_circ_ou, requestor_grp, usr_grp, circ_modifier, marc_type, marc_form, marc_vr_format, ref_flag, juvenile_flag) ); -CREATE OR REPLACE FUNCTION action.find_hold_matrix_matchpoint( pickup_ou INT, request_ou INT, match_item BIGINT, match_user INT, match_requestor INT ) RETURNS INT AS $func$ +CREATE OR REPLACE FUNCTION action.find_hold_matrix_matchpoint(pickup_ou integer, request_ou integer, match_item bigint, match_user integer, match_requestor integer) + RETURNS integer AS +$func$ DECLARE - current_requestor_group permission.grp_tree%ROWTYPE; requestor_object actor.usr%ROWTYPE; - user_object actor.usr%ROWTYPE; - item_object asset.copy%ROWTYPE; - item_cn_object asset.call_number%ROWTYPE; - rec_descriptor metabib.rec_descriptor%ROWTYPE; - current_mp_weight FLOAT; - matchpoint_weight FLOAT; - tmp_weight FLOAT; - current_mp config.hold_matrix_matchpoint%ROWTYPE; - matchpoint config.hold_matrix_matchpoint%ROWTYPE; + user_object actor.usr%ROWTYPE; + item_object asset.copy%ROWTYPE; + item_cn_object asset.call_number%ROWTYPE; + rec_descriptor metabib.rec_descriptor%ROWTYPE; + matchpoint config.hold_matrix_matchpoint%ROWTYPE; + weights config.hold_matrix_weights%ROWTYPE; + denominator INT; BEGIN - SELECT INTO user_object * FROM actor.usr WHERE id = match_user; - SELECT INTO requestor_object * FROM actor.usr WHERE id = match_requestor; - SELECT INTO item_object * FROM asset.copy WHERE id = match_item; - SELECT INTO item_cn_object * FROM asset.call_number WHERE id = item_object.call_number; - SELECT INTO rec_descriptor r.* FROM metabib.rec_descriptor r WHERE r.record = item_cn_object.record; + SELECT INTO user_object * FROM actor.usr WHERE id = match_user; + SELECT INTO requestor_object * FROM actor.usr WHERE id = match_requestor; + SELECT INTO item_object * FROM asset.copy WHERE id = match_item; + SELECT INTO item_cn_object * FROM asset.call_number WHERE id = item_object.call_number; + SELECT INTO rec_descriptor * FROM metabib.rec_descriptor WHERE record = item_cn_object.record; - PERFORM * FROM config.internal_flag WHERE name = 'circ.holds.usr_not_requestor' AND enabled; + -- The item's owner should probably be the one determining if the item is holdable + -- How to decide that is debatable. Decided to default to the circ library (where the item lives) + -- This flag will allow for setting it to the owning library (where the call number "lives") + PERFORM * FROM config.internal_flag WHERE name = 'circ.holds.weight_owner_not_circ' AND enabled; + -- Grab the closest set circ weight setting. + -- If we get a null set a default weighting will be pulled via COALESCE in the query. IF NOT FOUND THEN - SELECT INTO current_requestor_group * FROM permission.grp_tree WHERE id = requestor_object.profile; + -- Default to circ library + SELECT INTO weights hw.* + FROM config.weight_assoc wa + JOIN config.hold_matrix_weights hw ON (hw.id = wa.hold_weights) + JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) d ON (wa.org_unit = d.id) + WHERE active + ORDER BY d.distance + LIMIT 1; ELSE - SELECT INTO current_requestor_group * FROM permission.grp_tree WHERE id = user_object.profile; + -- Flag is set, use owning library + SELECT INTO weights hw.* + FROM config.weight_assoc wa + JOIN config.hold_matrix_weights hw ON (hw.id = wa.hold_weights) + JOIN actor.org_unit_ancestors_distance( cn_object.owning_lib ) d ON (wa.org_unit = d.id) + WHERE active + ORDER BY d.distance + LIMIT 1; END IF; - LOOP - -- for each potential matchpoint for this ou and group ... - FOR current_mp IN - SELECT m.* - FROM config.hold_matrix_matchpoint m - WHERE m.requestor_grp = current_requestor_group.id AND m.active - ORDER BY CASE WHEN m.circ_modifier IS NOT NULL THEN 16 ELSE 0 END + - CASE WHEN m.juvenile_flag IS NOT NULL THEN 16 ELSE 0 END + - CASE WHEN m.marc_type IS NOT NULL THEN 8 ELSE 0 END + - CASE WHEN m.marc_form IS NOT NULL THEN 4 ELSE 0 END + - CASE WHEN m.marc_vr_format IS NOT NULL THEN 2 ELSE 0 END + - CASE WHEN m.ref_flag IS NOT NULL THEN 1 ELSE 0 END DESC LOOP + -- No weights? Bad admin! Defaults to handle that anyway. + IF weights.id IS NULL THEN + weights.user_home_ou := 5; + weights.request_ou := 5; + weights.pickup_ou := 5; + weights.item_owning_ou := 5; + weights.item_circ_ou := 5; + weights.usr_grp := 7; + weights.requestor_grp := 8; + weights.circ_modifier := 4; + weights.marc_type := 3; + weights.marc_form := 2; + weights.marc_vr_format := 1; + weights.juvenile_flag := 4; + weights.ref_flag := 0; + END IF; - IF NOT current_mp.strict_ou_match THEN - current_mp_weight := 5.0; - ELSE - current_mp_weight := 0.0; - END IF; + -- Determine the max (expected) depth (+1) of the org tree and max depth of the permisson tree + -- If you break your org tree with funky parenting this may be wrong + -- Note: This CTE is duplicated in the find_circ_matrix_matchpoint function, and it may be a good idea to split it off to a function + -- We use one denominator for all tree-based checks for when permission groups and org units have the same weighting + WITH all_distance(distance) AS ( + SELECT depth AS distance FROM actor.org_unit_type + UNION + SELECT distance AS distance FROM permission.grp_ancestors_distance((SELECT id FROM permission.grp_tree WHERE parent IS NULL)) + ) + SELECT INTO denominator MAX(distance) + 1 FROM all_distance; - IF current_mp.circ_modifier IS NOT NULL THEN - CONTINUE WHEN current_mp.circ_modifier <> item_object.circ_modifier OR item_object.circ_modifier IS NULL; - END IF; + -- To ATTEMPT to make this work like it used to, make it reverse the user/requestor profile ids. + -- This may be better implemented as part of the upgrade script? + -- Set usr_grp = requestor_grp, requestor_grp = 1 or something when this flag is already set + -- Then remove this flag, of course. + PERFORM * FROM config.internal_flag WHERE name = 'circ.holds.usr_not_requestor' AND enabled; - IF current_mp.marc_type IS NOT NULL THEN - IF item_object.circ_as_type IS NOT NULL THEN - CONTINUE WHEN current_mp.marc_type <> item_object.circ_as_type; - ELSE - CONTINUE WHEN current_mp.marc_type <> rec_descriptor.item_type; - END IF; - END IF; + IF FOUND THEN + -- Note: This, to me, is REALLY hacky. I put it in anyway. + -- If you can't tell, this is a single call swap on two variables. + SELECT INTO user_object.profile, requestor_object.profile + requestor_object.profile, user_object.profile; + END IF; - IF current_mp.marc_form IS NOT NULL THEN - CONTINUE WHEN current_mp.marc_form <> rec_descriptor.item_form; - END IF; + -- Select the winning matchpoint into the matchpoint variable for returning + SELECT INTO matchpoint m.* + FROM config.hold_matrix_matchpoint m + /*LEFT*/ JOIN permission.grp_ancestors_distance( requestor_object.profile ) rpgad ON m.requestor_grp = rpgad.id + LEFT JOIN permission.grp_ancestors_distance( user_object.profile ) upgad ON m.usr_grp = upgad.id + LEFT JOIN actor.org_unit_ancestors_distance( pickup_ou ) puoua ON m.pickup_ou = puoua.id + LEFT JOIN actor.org_unit_ancestors_distance( request_ou ) rqoua ON m.request_ou = rqoua.id + LEFT JOIN actor.org_unit_ancestors_distance( item_cn_object.owning_lib ) cnoua ON m.item_owning_ou = cnoua.id + LEFT JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) iooua ON m.item_circ_ou = iooua.id + LEFT JOIN actor.org_unit_ancestors_distance( user_object.home_ou ) uhoua ON m.user_home_ou = uhoua.id + WHERE m.active + -- Permission Groups + -- AND (m.requestor_grp IS NULL OR upgad.id IS NOT NULL) -- Optional Requestor Group? + AND (m.usr_grp IS NULL OR upgad.id IS NOT NULL) + -- Org Units + AND (m.pickup_ou IS NULL OR (puoua.id IS NOT NULL AND (puoua.distance = 0 OR NOT m.strict_ou_match))) + AND (m.request_ou IS NULL OR (rqoua.id IS NOT NULL AND (rqoua.distance = 0 OR NOT m.strict_ou_match))) + AND (m.item_owning_ou IS NULL OR (cnoua.id IS NOT NULL AND (cnoua.distance = 0 OR NOT m.strict_ou_match))) + AND (m.item_circ_ou IS NULL OR (iooua.id IS NOT NULL AND (iooua.distance = 0 OR NOT m.strict_ou_match))) + AND (m.user_home_ou IS NULL OR (uhoua.id IS NOT NULL AND (uhoua.distance = 0 OR NOT m.strict_ou_match))) + -- Static User Checks + AND (m.juvenile_flag IS NULL OR m.juvenile_flag = user_object.juvenile) + -- Static Item Checks + AND (m.circ_modifier IS NULL OR m.circ_modifier = item_object.circ_modifier) + AND (m.marc_type IS NULL OR m.marc_type = COALESCE(item_object.circ_as_type, rec_descriptor.item_type)) + AND (m.marc_form IS NULL OR m.marc_form = rec_descriptor.item_form) + AND (m.marc_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 rpgad.distance IS NOT NULL THEN 2^(2*weights.requestor_grp - (rpgad.distance/denominator)) ELSE 0 END + + CASE WHEN upgad.distance IS NOT NULL THEN 2^(2*weights.usr_grp - (upgad.distance/denominator)) ELSE 0 END + + -- Org Units + CASE WHEN puoua.distance IS NOT NULL THEN 2^(2*weights.pickup_ou - (puoua.distance/denominator)) ELSE 0 END + + CASE WHEN rqoua.distance IS NOT NULL THEN 2^(2*weights.request_ou - (rqoua.distance/denominator)) ELSE 0 END + + CASE WHEN cnoua.distance IS NOT NULL THEN 2^(2*weights.item_owning_ou - (cnoua.distance/denominator)) ELSE 0 END + + CASE WHEN iooua.distance IS NOT NULL THEN 2^(2*weights.item_circ_ou - (iooua.distance/denominator)) ELSE 0 END + + CASE WHEN uhoua.distance IS NOT NULL THEN 2^(2*weights.user_home_ou - (uhoua.distance/denominator)) ELSE 0 END + + -- Static User Checks -- Note: 4^x is equiv to 2^(2*x) + CASE WHEN m.juvenile_flag IS NOT NULL THEN 4^weights.juvenile_flag ELSE 0 END + + -- Static Item Checks + CASE WHEN m.circ_modifier IS NOT NULL THEN 4^weights.circ_modifier ELSE 0 END + + CASE WHEN m.marc_type IS NOT NULL THEN 4^weights.marc_type ELSE 0 END + + CASE WHEN m.marc_form IS NOT NULL THEN 4^weights.marc_form ELSE 0 END + + CASE WHEN m.marc_vr_format IS NOT NULL THEN 4^weights.marc_vr_format ELSE 0 END + + CASE WHEN m.ref_flag IS NOT NULL THEN 4^weights.ref_flag ELSE 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; - IF current_mp.marc_vr_format IS NOT NULL THEN - CONTINUE WHEN current_mp.marc_vr_format <> rec_descriptor.vr_format; - END IF; - - IF current_mp.juvenile_flag IS NOT NULL THEN - CONTINUE WHEN current_mp.juvenile_flag <> user_object.juvenile; - END IF; - - IF current_mp.ref_flag IS NOT NULL THEN - CONTINUE WHEN current_mp.ref_flag <> item_object.ref; - END IF; - - - -- caclulate the rule match weight - IF current_mp.item_owning_ou IS NOT NULL THEN - CONTINUE WHEN current_mp.item_owning_ou NOT IN (SELECT (actor.org_unit_ancestors(item_cn_object.owning_lib)).id); - IF NOT current_mp.strict_ou_match THEN - SELECT INTO tmp_weight 1.0 / (actor.org_unit_proximity(current_mp.item_owning_ou, item_cn_object.owning_lib)::FLOAT + 1.0)::FLOAT; - ELSE - CONTINUE WHEN current_mp.item_owning_ou <> item_cn_object.owning_lib; - tmp_weight := CASE WHEN current_mp.item_owning_ou = item_cn_object.owning_lib THEN 1.0 ELSE 0.0 END; - END IF; - current_mp_weight := current_mp_weight - tmp_weight; - END IF; - - IF current_mp.item_circ_ou IS NOT NULL THEN - CONTINUE WHEN current_mp.item_circ_ou NOT IN (SELECT (actor.org_unit_ancestors(item_object.circ_lib)).id); - IF NOT current_mp.strict_ou_match THEN - SELECT INTO tmp_weight 1.0 / (actor.org_unit_proximity(current_mp.item_circ_ou, item_object.circ_lib)::FLOAT + 1.0)::FLOAT; - ELSE - CONTINUE WHEN current_mp.item_circ_ou <> item_object.circ_lib; - tmp_weight := CASE WHEN current_mp.item_circ_ou = item_object.circ_lib THEN 1.0 ELSE 0.0 END; - END IF; - current_mp_weight := current_mp_weight - tmp_weight; - END IF; - - IF current_mp.pickup_ou IS NOT NULL THEN - CONTINUE WHEN current_mp.pickup_ou NOT IN (SELECT (actor.org_unit_ancestors(pickup_ou)).id); - IF NOT current_mp.strict_ou_match THEN - SELECT INTO tmp_weight 1.0 / (actor.org_unit_proximity(current_mp.pickup_ou, pickup_ou)::FLOAT + 1.0)::FLOAT; - ELSE - CONTINUE WHEN current_mp.pickup_ou <> pickup_ou; - tmp_weight := CASE WHEN current_mp.pickup_ou = pickiup_ou THEN 1.0 ELSE 0.0 END; - END IF; - current_mp_weight := current_mp_weight - tmp_weight; - END IF; - - IF current_mp.request_ou IS NOT NULL THEN - CONTINUE WHEN current_mp.request_ou NOT IN (SELECT (actor.org_unit_ancestors(request_ou)).id); - IF NOT current_mp.strict_ou_match THEN - SELECT INTO tmp_weight 1.0 / (actor.org_unit_proximity(current_mp.request_ou, request_ou)::FLOAT + 1.0)::FLOAT; - ELSE - CONTINUE WHEN current_mp.request_ou <> request_ou; - tmp_weight := CASE WHEN current_mp.request_ou = request_ou THEN 1.0 ELSE 0.0 END; - END IF; - current_mp_weight := current_mp_weight - tmp_weight; - END IF; - - IF current_mp.user_home_ou IS NOT NULL THEN - CONTINUE WHEN current_mp.user_home_ou NOT IN (SELECT (actor.org_unit_ancestors(user_object.home_ou)).id); - IF NOT current_mp.strict_ou_match THEN - SELECT INTO tmp_weight 1.0 / (actor.org_unit_proximity(current_mp.user_home_ou, user_object.home_ou)::FLOAT + 1.0)::FLOAT; - ELSE - CONTINUE WHEN current_mp.user_home_ou <> user_object.home_ou; - tmp_weight := CASE WHEN current_mp.user_home_ou = user_object.home_ou THEN 1.0 ELSE 0.0 END; - END IF; - current_mp_weight := current_mp_weight - tmp_weight; - END IF; - - -- set the matchpoint if we found the best one - IF matchpoint_weight IS NULL OR matchpoint_weight > current_mp_weight THEN - matchpoint = current_mp; - matchpoint_weight = current_mp_weight; - END IF; - - END LOOP; - - EXIT WHEN current_requestor_group.parent IS NULL OR matchpoint.id IS NOT NULL; - - SELECT INTO current_requestor_group * FROM permission.grp_tree WHERE id = current_requestor_group.parent; - END LOOP; - + -- Return just the ID for now RETURN matchpoint.id; END; -$func$ LANGUAGE plpgsql; +$func$ LANGUAGE 'plpgsql'; - 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; Index: Open-ILS/src/sql/Pg/006.schema.permissions.sql =================================================================== --- Open-ILS/src/sql/Pg/006.schema.permissions.sql (revision 19247) +++ Open-ILS/src/sql/Pg/006.schema.permissions.sql (working copy) @@ -101,6 +101,27 @@ END, a.name; $$ LANGUAGE SQL STABLE; +CREATE OR REPLACE FUNCTION permission.grp_ancestors_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$ + WITH RECURSIVE grp_ancestors_distance(id, distance) AS ( + SELECT $1, 0 + UNION + SELECT pgt.parent, gad.distance+1 + FROM permission.grp_tree pgt JOIN grp_ancestors_distance gad ON pgt.id = gad.id + WHERE pgt.parent IS NOT NULL + ) + SELECT * FROM grp_ancestors_distance; +$$ LANGUAGE SQL STABLE; + +CREATE OR REPLACE FUNCTION permission.grp_descendants_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$ + WITH RECURSIVE grp_descendants_distance(id, distance) AS ( + SELECT $1, 0 + UNION + SELECT pgt.id, gdd.distance+1 + FROM permission.grp_tree pgt JOIN grp_descendants_distance gdd ON pgt.parent = gdd.id + ) + SELECT * FROM grp_descendants_distance; +$$ LANGUAGE SQL STABLE; + CREATE OR REPLACE FUNCTION permission.usr_perms ( INT ) RETURNS SETOF permission.usr_perm_map AS $$ SELECT DISTINCT ON (usr,perm) * FROM ( Index: Open-ILS/xul/staff_client/chrome/content/main/menu.js =================================================================== --- Open-ILS/xul/staff_client/chrome/content/main/menu.js (revision 19247) +++ Open-ILS/xul/staff_client/chrome/content/main/menu.js (working copy) @@ -629,6 +629,18 @@ ['oncommand'], function() { open_eg_web_page('conify/global/config/rule_age_hold_protect'); } ], + 'cmd_server_admin_config_circ_weights' : [ + ['oncommand'], + function() { open_eg_web_page('conify/global/config/circ_matrix_weights'); } + ], + 'cmd_server_admin_config_hold_weights' : [ + ['oncommand'], + function() { open_eg_web_page('conify/global/config/hold_matrix_weights'); } + ], + 'cmd_server_admin_config_weight_assoc' : [ + ['oncommand'], + function() { open_eg_web_page('conify/global/config/weight_assoc'); } + ], 'cmd_local_admin_external_text_editor' : [ ['oncommand'], function() { Index: Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul =================================================================== --- Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul (revision 19247) +++ Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul (working copy) @@ -173,6 +173,9 @@ + + + @@ -400,6 +403,9 @@ + + + Index: Open-ILS/web/opac/locale/en-US/lang.dtd =================================================================== --- Open-ILS/web/opac/locale/en-US/lang.dtd (revision 19247) +++ Open-ILS/web/opac/locale/en-US/lang.dtd (working copy) @@ -717,6 +717,9 @@ + + + Index: Open-ILS/web/templates/default/conify/global/config/weight_assoc.tt2 =================================================================== --- Open-ILS/web/templates/default/conify/global/config/weight_assoc.tt2 (revision 0) +++ Open-ILS/web/templates/default/conify/global/config/weight_assoc.tt2 (revision 0) @@ -0,0 +1,28 @@ +[% WRAPPER default/base.tt2 %] +[% ctx.page_title = 'Matrix Weight Associations' %] +
+
+
Matrix Weight Associations
+
+ + +
+
+
+ + + + +[% END %] + + Index: Open-ILS/web/templates/default/conify/global/config/circ_matrix_weights.tt2 =================================================================== --- Open-ILS/web/templates/default/conify/global/config/circ_matrix_weights.tt2 (revision 0) +++ Open-ILS/web/templates/default/conify/global/config/circ_matrix_weights.tt2 (revision 0) @@ -0,0 +1,28 @@ +[% WRAPPER default/base.tt2 %] +[% ctx.page_title = 'Circ Matrix Weights' %] +
+
+
Circ Matrix Weights
+
+ + +
+
+
+
+ + + +[% END %] + + Index: Open-ILS/web/templates/default/conify/global/config/hold_matrix_matchpoint.tt2 =================================================================== --- Open-ILS/web/templates/default/conify/global/config/hold_matrix_matchpoint.tt2 (revision 19247) +++ Open-ILS/web/templates/default/conify/global/config/hold_matrix_matchpoint.tt2 (working copy) @@ -9,7 +9,6 @@ autoHeight='true' dojoType="openils.widget.AutoGrid" fieldOrder="['id', 'strict_ou_match', 'user_home_ou', 'request_ou', 'pickup_ou', 'item_owning_ou', 'item_circ_ou', 'requestor_grp', 'circ_modifier']" - suppressFields="['usr_grp']" defaultCellWidth='"auto"' query="{id: '*'}" fmClass='chmm' Index: Open-ILS/web/templates/default/conify/global/config/hold_matrix_weights.tt2 =================================================================== --- Open-ILS/web/templates/default/conify/global/config/hold_matrix_weights.tt2 (revision 0) +++ Open-ILS/web/templates/default/conify/global/config/hold_matrix_weights.tt2 (revision 0) @@ -0,0 +1,28 @@ +[% WRAPPER default/base.tt2 %] +[% ctx.page_title = 'Hold Matrix Weights' %] +
+
+
Hold Matrix Weights
+
+ + +
+
+
+
+ + + +[% END %] + + Index: Open-ILS/examples/fm_IDL.xml =================================================================== --- Open-ILS/examples/fm_IDL.xml (revision 19247) +++ Open-ILS/examples/fm_IDL.xml (working copy) @@ -1003,6 +1003,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1060,6 +1139,7 @@ + @@ -1081,6 +1161,7 @@ +