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 @@