3 INSERT INTO config.upgrade_log (version) VALUES ('0479');
5 CREATE OR REPLACE FUNCTION permission.grp_ancestors_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$
6 WITH RECURSIVE grp_ancestors_distance(id, distance) AS (
9 SELECT pgt.parent, gad.distance+1
10 FROM permission.grp_tree pgt JOIN grp_ancestors_distance gad ON pgt.id = gad.id
11 WHERE pgt.parent IS NOT NULL
13 SELECT * FROM grp_ancestors_distance;
14 $$ LANGUAGE SQL STABLE;
16 CREATE OR REPLACE FUNCTION permission.grp_descendants_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$
17 WITH RECURSIVE grp_descendants_distance(id, distance) AS (
20 SELECT pgt.id, gdd.distance+1
21 FROM permission.grp_tree pgt JOIN grp_descendants_distance gdd ON pgt.parent = gdd.id
23 SELECT * FROM grp_descendants_distance;
24 $$ LANGUAGE SQL STABLE;
26 CREATE OR REPLACE FUNCTION actor.org_unit_ancestors_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$
27 WITH RECURSIVE org_unit_ancestors_distance(id, distance) AS (
30 SELECT ou.parent_ou, ouad.distance+1
31 FROM actor.org_unit ou JOIN org_unit_ancestors_distance ouad ON ou.id = ouad.id
32 WHERE ou.parent_ou IS NOT NULL
34 SELECT * FROM org_unit_ancestors_distance;
35 $$ LANGUAGE SQL STABLE;
37 CREATE OR REPLACE FUNCTION actor.org_unit_descendants_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$
38 WITH RECURSIVE org_unit_descendants_distance(id, distance) AS (
41 SELECT ou.id, oudd.distance+1
42 FROM actor.org_unit ou JOIN org_unit_descendants_distance oudd ON ou.parent_ou = oudd.id
44 SELECT * FROM org_unit_descendants_distance;
45 $$ LANGUAGE SQL STABLE;
47 ALTER TABLE config.circ_matrix_matchpoint
48 ADD COLUMN user_home_ou INT REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED;
50 CREATE TABLE config.circ_matrix_weights (
51 id SERIAL PRIMARY KEY,
52 name TEXT NOT NULL UNIQUE,
53 org_unit NUMERIC(6,2) NOT NULL,
54 grp NUMERIC(6,2) NOT NULL,
55 circ_modifier NUMERIC(6,2) NOT NULL,
56 marc_type NUMERIC(6,2) NOT NULL,
57 marc_form NUMERIC(6,2) NOT NULL,
58 marc_vr_format NUMERIC(6,2) NOT NULL,
59 copy_circ_lib NUMERIC(6,2) NOT NULL,
60 copy_owning_lib NUMERIC(6,2) NOT NULL,
61 user_home_ou NUMERIC(6,2) NOT NULL,
62 ref_flag NUMERIC(6,2) NOT NULL,
63 juvenile_flag NUMERIC(6,2) NOT NULL,
64 is_renewal NUMERIC(6,2) NOT NULL,
65 usr_age_lower_bound NUMERIC(6,2) NOT NULL,
66 usr_age_upper_bound NUMERIC(6,2) NOT NULL
69 CREATE TABLE config.hold_matrix_weights (
70 id SERIAL PRIMARY KEY,
71 name TEXT NOT NULL UNIQUE,
72 user_home_ou NUMERIC(6,2) NOT NULL,
73 request_ou NUMERIC(6,2) NOT NULL,
74 pickup_ou NUMERIC(6,2) NOT NULL,
75 item_owning_ou NUMERIC(6,2) NOT NULL,
76 item_circ_ou NUMERIC(6,2) NOT NULL,
77 usr_grp NUMERIC(6,2) NOT NULL,
78 requestor_grp NUMERIC(6,2) NOT NULL,
79 circ_modifier NUMERIC(6,2) NOT NULL,
80 marc_type NUMERIC(6,2) NOT NULL,
81 marc_form NUMERIC(6,2) NOT NULL,
82 marc_vr_format NUMERIC(6,2) NOT NULL,
83 juvenile_flag NUMERIC(6,2) NOT NULL,
84 ref_flag NUMERIC(6,2) NOT NULL
87 CREATE TABLE config.weight_assoc (
88 id SERIAL PRIMARY KEY,
90 org_unit INT NOT NULL REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
91 circ_weights INT REFERENCES config.circ_matrix_weights (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED,
92 hold_weights INT REFERENCES config.hold_matrix_weights (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED
94 CREATE UNIQUE INDEX cwa_one_active_per_ou ON config.weight_assoc (org_unit) WHERE active;
96 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$
98 user_object actor.usr%ROWTYPE;
99 item_object asset.copy%ROWTYPE;
100 cn_object asset.call_number%ROWTYPE;
101 rec_descriptor metabib.rec_descriptor%ROWTYPE;
102 matchpoint config.circ_matrix_matchpoint%ROWTYPE;
103 weights config.circ_matrix_weights%ROWTYPE;
107 SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
108 SELECT INTO item_object * FROM asset.copy WHERE id = match_item;
109 SELECT INTO cn_object * FROM asset.call_number WHERE id = item_object.call_number;
110 SELECT INTO rec_descriptor * FROM metabib.rec_descriptor WHERE record = cn_object.record;
112 -- Pre-generate this so we only calc it once
113 IF user_object.dob IS NOT NULL THEN
114 SELECT INTO user_age age(user_object.dob);
117 -- Grab the closest set circ weight setting.
118 SELECT INTO weights cw.*
119 FROM config.weight_assoc wa
120 JOIN config.circ_matrix_weights cw ON (cw.id = wa.circ_weights)
121 JOIN actor.org_unit_ancestors_distance( context_ou ) d ON (wa.org_unit = d.id)
126 -- No weights? Bad admin! Defaults to handle that anyway.
127 IF weights.id IS NULL THEN
129 weights.org_unit := 10;
130 weights.circ_modifier := 5;
131 weights.marc_type := 4;
132 weights.marc_form := 3;
133 weights.marc_vr_format := 2;
134 weights.copy_circ_lib := 8;
135 weights.copy_owning_lib := 8;
136 weights.user_home_ou := 8;
137 weights.ref_flag := 1;
138 weights.juvenile_flag := 6;
139 weights.is_renewal := 7;
140 weights.usr_age_lower_bound := 0;
141 weights.usr_age_upper_bound := 0;
144 -- Determine the max (expected) depth (+1) of the org tree and max depth of the permisson tree
145 -- If you break your org tree with funky parenting this may be wrong
146 -- 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
147 -- We use one denominator for all tree-based checks for when permission groups and org units have the same weighting
148 WITH all_distance(distance) AS (
149 SELECT depth AS distance FROM actor.org_unit_type
151 SELECT distance AS distance FROM permission.grp_ancestors_distance((SELECT id FROM permission.grp_tree WHERE parent IS NULL))
153 SELECT INTO denominator MAX(distance) + 1 FROM all_distance;
155 -- Select the winning matchpoint into the matchpoint variable for returning
156 SELECT INTO matchpoint m.*
157 FROM config.circ_matrix_matchpoint m
158 /*LEFT*/ JOIN permission.grp_ancestors_distance( user_object.profile ) upgad ON m.grp = upgad.id
159 /*LEFT*/ JOIN actor.org_unit_ancestors_distance( context_ou ) ctoua ON m.org_unit = ctoua.id
160 LEFT JOIN actor.org_unit_ancestors_distance( cn_object.owning_lib ) cnoua ON m.copy_owning_lib = cnoua.id
161 LEFT JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) iooua ON m.copy_circ_lib = iooua.id
162 LEFT JOIN actor.org_unit_ancestors_distance( user_object.home_ou ) uhoua ON m.user_home_ou = uhoua.id
165 -- AND (m.grp IS NULL OR upgad.id IS NOT NULL) -- Optional Permission Group?
167 -- AND (m.org_unit IS NULL OR ctoua.id IS NOT NULL) -- Optional Org Unit?
168 AND (m.copy_owning_lib IS NULL OR cnoua.id IS NOT NULL)
169 AND (m.copy_circ_lib IS NULL OR iooua.id IS NOT NULL)
170 AND (m.user_home_ou IS NULL OR uhoua.id IS NOT NULL)
172 AND (m.is_renewal IS NULL OR m.is_renewal = renewal)
173 -- Static User Checks
174 AND (m.juvenile_flag IS NULL OR m.juvenile_flag = user_object.juvenile)
175 AND (m.usr_age_lower_bound IS NULL OR (user_age IS NOT NULL AND m.usr_age_lower_bound < user_age))
176 AND (m.usr_age_upper_bound IS NULL OR (user_age IS NOT NULL AND m.usr_age_upper_bound > user_age))
177 -- Static Item Checks
178 AND (m.circ_modifier IS NULL OR m.circ_modifier = item_object.circ_modifier)
179 AND (m.marc_type IS NULL OR m.marc_type = COALESCE(item_object.circ_as_type, rec_descriptor.item_type))
180 AND (m.marc_form IS NULL OR m.marc_form = rec_descriptor.item_form)
181 AND (m.marc_vr_format IS NULL OR m.marc_vr_format = rec_descriptor.vr_format)
182 AND (m.ref_flag IS NULL OR m.ref_flag = item_object.ref)
185 CASE WHEN upgad.distance IS NOT NULL THEN 2^(2*weights.grp - (upgad.distance/denominator)) ELSE 0 END +
187 CASE WHEN ctoua.distance IS NOT NULL THEN 2^(2*weights.org_unit - (ctoua.distance/denominator)) ELSE 0 END +
188 CASE WHEN cnoua.distance IS NOT NULL THEN 2^(2*weights.copy_owning_lib - (cnoua.distance/denominator)) ELSE 0 END +
189 CASE WHEN iooua.distance IS NOT NULL THEN 2^(2*weights.copy_circ_lib - (iooua.distance/denominator)) ELSE 0 END +
190 CASE WHEN uhoua.distance IS NOT NULL THEN 2^(2*weights.user_home_ou - (uhoua.distance/denominator)) ELSE 0 END +
191 -- Circ Type -- Note: 4^x is equiv to 2^(2*x)
192 CASE WHEN m.is_renewal IS NOT NULL THEN 4^weights.is_renewal ELSE 0 END +
193 -- Static User Checks
194 CASE WHEN m.juvenile_flag IS NOT NULL THEN 4^weights.juvenile_flag ELSE 0 END +
195 CASE WHEN m.usr_age_lower_bound IS NOT NULL THEN 4^weights.usr_age_lower_bound ELSE 0 END +
196 CASE WHEN m.usr_age_upper_bound IS NOT NULL THEN 4^weights.usr_age_upper_bound ELSE 0 END +
197 -- Static Item Checks
198 CASE WHEN m.circ_modifier IS NOT NULL THEN 4^weights.circ_modifier ELSE 0 END +
199 CASE WHEN m.marc_type IS NOT NULL THEN 4^weights.marc_type ELSE 0 END +
200 CASE WHEN m.marc_form IS NOT NULL THEN 4^weights.marc_form ELSE 0 END +
201 CASE WHEN m.marc_vr_format IS NOT NULL THEN 4^weights.marc_vr_format ELSE 0 END +
202 CASE WHEN m.ref_flag IS NOT NULL THEN 4^weights.ref_flag ELSE 0 END DESC,
203 -- Final sort on id, so that if two rules have the same sorting in the previous sort they have a defined order
204 -- This prevents "we changed the table order by updating a rule, and we started getting different results"
207 -- Return the entire matchpoint
210 $func$ LANGUAGE plpgsql;
212 CREATE OR REPLACE FUNCTION action.find_hold_matrix_matchpoint(pickup_ou integer, request_ou integer, match_item bigint, match_user integer, match_requestor integer)
216 requestor_object actor.usr%ROWTYPE;
217 user_object actor.usr%ROWTYPE;
218 item_object asset.copy%ROWTYPE;
219 item_cn_object asset.call_number%ROWTYPE;
220 rec_descriptor metabib.rec_descriptor%ROWTYPE;
221 matchpoint config.hold_matrix_matchpoint%ROWTYPE;
222 weights config.hold_matrix_weights%ROWTYPE;
225 SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
226 SELECT INTO requestor_object * FROM actor.usr WHERE id = match_requestor;
227 SELECT INTO item_object * FROM asset.copy WHERE id = match_item;
228 SELECT INTO item_cn_object * FROM asset.call_number WHERE id = item_object.call_number;
229 SELECT INTO rec_descriptor * FROM metabib.rec_descriptor WHERE record = item_cn_object.record;
231 -- The item's owner should probably be the one determining if the item is holdable
232 -- How to decide that is debatable. Decided to default to the circ library (where the item lives)
233 -- This flag will allow for setting it to the owning library (where the call number "lives")
234 PERFORM * FROM config.internal_flag WHERE name = 'circ.holds.weight_owner_not_circ' AND enabled;
236 -- Grab the closest set circ weight setting.
238 -- Default to circ library
239 SELECT INTO weights hw.*
240 FROM config.weight_assoc wa
241 JOIN config.hold_matrix_weights hw ON (hw.id = wa.hold_weights)
242 JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) d ON (wa.org_unit = d.id)
247 -- Flag is set, use owning library
248 SELECT INTO weights hw.*
249 FROM config.weight_assoc wa
250 JOIN config.hold_matrix_weights hw ON (hw.id = wa.hold_weights)
251 JOIN actor.org_unit_ancestors_distance( cn_object.owning_lib ) d ON (wa.org_unit = d.id)
257 -- No weights? Bad admin! Defaults to handle that anyway.
258 IF weights.id IS NULL THEN
259 weights.user_home_ou := 5;
260 weights.request_ou := 5;
261 weights.pickup_ou := 5;
262 weights.item_owning_ou := 5;
263 weights.item_circ_ou := 5;
264 weights.usr_grp := 7;
265 weights.requestor_grp := 8;
266 weights.circ_modifier := 4;
267 weights.marc_type := 3;
268 weights.marc_form := 2;
269 weights.marc_vr_format := 1;
270 weights.juvenile_flag := 4;
271 weights.ref_flag := 0;
274 -- Determine the max (expected) depth (+1) of the org tree and max depth of the permisson tree
275 -- If you break your org tree with funky parenting this may be wrong
276 -- 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
277 -- We use one denominator for all tree-based checks for when permission groups and org units have the same weighting
278 WITH all_distance(distance) AS (
279 SELECT depth AS distance FROM actor.org_unit_type
281 SELECT distance AS distance FROM permission.grp_ancestors_distance((SELECT id FROM permission.grp_tree WHERE parent IS NULL))
283 SELECT INTO denominator MAX(distance) + 1 FROM all_distance;
285 -- To ATTEMPT to make this work like it used to, make it reverse the user/requestor profile ids.
286 -- This may be better implemented as part of the upgrade script?
287 -- Set usr_grp = requestor_grp, requestor_grp = 1 or something when this flag is already set
288 -- Then remove this flag, of course.
289 PERFORM * FROM config.internal_flag WHERE name = 'circ.holds.usr_not_requestor' AND enabled;
292 -- Note: This, to me, is REALLY hacky. I put it in anyway.
293 -- If you can't tell, this is a single call swap on two variables.
294 SELECT INTO user_object.profile, requestor_object.profile
295 requestor_object.profile, user_object.profile;
298 -- Select the winning matchpoint into the matchpoint variable for returning
299 SELECT INTO matchpoint m.*
300 FROM config.hold_matrix_matchpoint m
301 /*LEFT*/ JOIN permission.grp_ancestors_distance( requestor_object.profile ) rpgad ON m.requestor_grp = rpgad.id
302 LEFT JOIN permission.grp_ancestors_distance( user_object.profile ) upgad ON m.usr_grp = upgad.id
303 LEFT JOIN actor.org_unit_ancestors_distance( pickup_ou ) puoua ON m.pickup_ou = puoua.id
304 LEFT JOIN actor.org_unit_ancestors_distance( request_ou ) rqoua ON m.request_ou = rqoua.id
305 LEFT JOIN actor.org_unit_ancestors_distance( item_cn_object.owning_lib ) cnoua ON m.item_owning_ou = cnoua.id
306 LEFT JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) iooua ON m.item_circ_ou = iooua.id
307 LEFT JOIN actor.org_unit_ancestors_distance( user_object.home_ou ) uhoua ON m.user_home_ou = uhoua.id
310 -- AND (m.requestor_grp IS NULL OR upgad.id IS NOT NULL) -- Optional Requestor Group?
311 AND (m.usr_grp IS NULL OR upgad.id IS NOT NULL)
313 AND (m.pickup_ou IS NULL OR (puoua.id IS NOT NULL AND (puoua.distance = 0 OR NOT m.strict_ou_match)))
314 AND (m.request_ou IS NULL OR (rqoua.id IS NOT NULL AND (rqoua.distance = 0 OR NOT m.strict_ou_match)))
315 AND (m.item_owning_ou IS NULL OR (cnoua.id IS NOT NULL AND (cnoua.distance = 0 OR NOT m.strict_ou_match)))
316 AND (m.item_circ_ou IS NULL OR (iooua.id IS NOT NULL AND (iooua.distance = 0 OR NOT m.strict_ou_match)))
317 AND (m.user_home_ou IS NULL OR (uhoua.id IS NOT NULL AND (uhoua.distance = 0 OR NOT m.strict_ou_match)))
318 -- Static User Checks
319 AND (m.juvenile_flag IS NULL OR m.juvenile_flag = user_object.juvenile)
320 -- Static Item Checks
321 AND (m.circ_modifier IS NULL OR m.circ_modifier = item_object.circ_modifier)
322 AND (m.marc_type IS NULL OR m.marc_type = COALESCE(item_object.circ_as_type, rec_descriptor.item_type))
323 AND (m.marc_form IS NULL OR m.marc_form = rec_descriptor.item_form)
324 AND (m.marc_vr_format IS NULL OR m.marc_vr_format = rec_descriptor.vr_format)
325 AND (m.ref_flag IS NULL OR m.ref_flag = item_object.ref)
328 CASE WHEN rpgad.distance IS NOT NULL THEN 2^(2*weights.requestor_grp - (rpgad.distance/denominator)) ELSE 0 END +
329 CASE WHEN upgad.distance IS NOT NULL THEN 2^(2*weights.usr_grp - (upgad.distance/denominator)) ELSE 0 END +
331 CASE WHEN puoua.distance IS NOT NULL THEN 2^(2*weights.pickup_ou - (puoua.distance/denominator)) ELSE 0 END +
332 CASE WHEN rqoua.distance IS NOT NULL THEN 2^(2*weights.request_ou - (rqoua.distance/denominator)) ELSE 0 END +
333 CASE WHEN cnoua.distance IS NOT NULL THEN 2^(2*weights.item_owning_ou - (cnoua.distance/denominator)) ELSE 0 END +
334 CASE WHEN iooua.distance IS NOT NULL THEN 2^(2*weights.item_circ_ou - (iooua.distance/denominator)) ELSE 0 END +
335 CASE WHEN uhoua.distance IS NOT NULL THEN 2^(2*weights.user_home_ou - (uhoua.distance/denominator)) ELSE 0 END +
336 -- Static User Checks -- Note: 4^x is equiv to 2^(2*x)
337 CASE WHEN m.juvenile_flag IS NOT NULL THEN 4^weights.juvenile_flag ELSE 0 END +
338 -- Static Item Checks
339 CASE WHEN m.circ_modifier IS NOT NULL THEN 4^weights.circ_modifier ELSE 0 END +
340 CASE WHEN m.marc_type IS NOT NULL THEN 4^weights.marc_type ELSE 0 END +
341 CASE WHEN m.marc_form IS NOT NULL THEN 4^weights.marc_form ELSE 0 END +
342 CASE WHEN m.marc_vr_format IS NOT NULL THEN 4^weights.marc_vr_format ELSE 0 END +
343 CASE WHEN m.ref_flag IS NOT NULL THEN 4^weights.ref_flag ELSE 0 END DESC,
344 -- Final sort on id, so that if two rules have the same sorting in the previous sort they have a defined order
345 -- This prevents "we changed the table order by updating a rule, and we started getting different results"
348 -- Return just the ID for now
349 RETURN matchpoint.id;
351 $func$ LANGUAGE 'plpgsql';
353 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
354 ('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),
355 ('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),
356 ('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),
357 ('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);
359 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
360 ('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),
361 ('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),
362 ('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),
363 ('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);
365 INSERT INTO config.weight_assoc(active, org_unit, circ_weights, hold_weights) VALUES