3 ALTER TABLE config.circ_matrix_weights ADD COLUMN marc_bib_level NUMERIC(6,2) NOT NULL DEFAULT 0.0;
5 UPDATE config.circ_matrix_weights
6 SET marc_bib_level = marc_vr_format;
8 ALTER TABLE config.hold_matrix_weights ADD COLUMN marc_bib_level NUMERIC(6, 2) NOT NULL DEFAULT 0.0;
10 UPDATE config.hold_matrix_weights
11 SET marc_bib_level = marc_vr_format;
13 ALTER TABLE config.circ_matrix_weights ALTER COLUMN marc_bib_level DROP DEFAULT;
15 ALTER TABLE config.hold_matrix_weights ALTER COLUMN marc_bib_level DROP DEFAULT;
17 ALTER TABLE config.circ_matrix_matchpoint ADD COLUMN marc_bib_level text;
19 ALTER TABLE config.hold_matrix_matchpoint ADD COLUMN marc_bib_level text;
21 CREATE OR REPLACE FUNCTION action.find_circ_matrix_matchpoint( context_ou INT, item_object asset.copy, user_object actor.usr, renewal BOOL ) RETURNS action.found_circ_matrix_matchpoint AS $func$
23 cn_object asset.call_number%ROWTYPE;
24 rec_descriptor metabib.rec_descriptor%ROWTYPE;
25 cur_matchpoint config.circ_matrix_matchpoint%ROWTYPE;
26 matchpoint config.circ_matrix_matchpoint%ROWTYPE;
27 weights config.circ_matrix_weights%ROWTYPE;
29 denominator NUMERIC(6,2);
31 result action.found_circ_matrix_matchpoint;
34 result.success = false;
37 SELECT INTO cn_object * FROM asset.call_number WHERE id = item_object.call_number;
38 SELECT INTO rec_descriptor * FROM metabib.rec_descriptor WHERE record = cn_object.record;
40 -- Pre-generate this so we only calc it once
41 IF user_object.dob IS NOT NULL THEN
42 SELECT INTO user_age age(user_object.dob);
45 -- Grab the closest set circ weight setting.
46 SELECT INTO weights cw.*
47 FROM config.weight_assoc wa
48 JOIN config.circ_matrix_weights cw ON (cw.id = wa.circ_weights)
49 JOIN actor.org_unit_ancestors_distance( context_ou ) d ON (wa.org_unit = d.id)
54 -- No weights? Bad admin! Defaults to handle that anyway.
55 IF weights.id IS NULL THEN
57 weights.org_unit := 10.0;
58 weights.circ_modifier := 5.0;
59 weights.marc_type := 4.0;
60 weights.marc_form := 3.0;
61 weights.marc_bib_level := 2.0;
62 weights.marc_vr_format := 2.0;
63 weights.copy_circ_lib := 8.0;
64 weights.copy_owning_lib := 8.0;
65 weights.user_home_ou := 8.0;
66 weights.ref_flag := 1.0;
67 weights.juvenile_flag := 6.0;
68 weights.is_renewal := 7.0;
69 weights.usr_age_lower_bound := 0.0;
70 weights.usr_age_upper_bound := 0.0;
73 -- Determine the max (expected) depth (+1) of the org tree and max depth of the permisson tree
74 -- If you break your org tree with funky parenting this may be wrong
75 -- 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
76 -- We use one denominator for all tree-based checks for when permission groups and org units have the same weighting
77 WITH all_distance(distance) AS (
78 SELECT depth AS distance FROM actor.org_unit_type
80 SELECT distance AS distance FROM permission.grp_ancestors_distance((SELECT id FROM permission.grp_tree WHERE parent IS NULL))
82 SELECT INTO denominator MAX(distance) + 1 FROM all_distance;
84 -- Loop over all the potential matchpoints
87 FROM config.circ_matrix_matchpoint m
88 /*LEFT*/ JOIN permission.grp_ancestors_distance( user_object.profile ) upgad ON m.grp = upgad.id
89 /*LEFT*/ JOIN actor.org_unit_ancestors_distance( context_ou ) ctoua ON m.org_unit = ctoua.id
90 LEFT JOIN actor.org_unit_ancestors_distance( cn_object.owning_lib ) cnoua ON m.copy_owning_lib = cnoua.id
91 LEFT JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) iooua ON m.copy_circ_lib = iooua.id
92 LEFT JOIN actor.org_unit_ancestors_distance( user_object.home_ou ) uhoua ON m.user_home_ou = uhoua.id
95 -- AND (m.grp IS NULL OR upgad.id IS NOT NULL) -- Optional Permission Group?
97 -- AND (m.org_unit IS NULL OR ctoua.id IS NOT NULL) -- Optional Org Unit?
98 AND (m.copy_owning_lib IS NULL OR cnoua.id IS NOT NULL)
99 AND (m.copy_circ_lib IS NULL OR iooua.id IS NOT NULL)
100 AND (m.user_home_ou IS NULL OR uhoua.id IS NOT NULL)
102 AND (m.is_renewal IS NULL OR m.is_renewal = renewal)
103 -- Static User Checks
104 AND (m.juvenile_flag IS NULL OR m.juvenile_flag = user_object.juvenile)
105 AND (m.usr_age_lower_bound IS NULL OR (user_age IS NOT NULL AND m.usr_age_lower_bound < user_age))
106 AND (m.usr_age_upper_bound IS NULL OR (user_age IS NOT NULL AND m.usr_age_upper_bound > user_age))
107 -- Static Item Checks
108 AND (m.circ_modifier IS NULL OR m.circ_modifier = item_object.circ_modifier)
109 AND (m.marc_type IS NULL OR m.marc_type = COALESCE(item_object.circ_as_type, rec_descriptor.item_type))
110 AND (m.marc_form IS NULL OR m.marc_form = rec_descriptor.item_form)
111 AND (m.marc_bib_level IS NULL OR m.marc_bib_level = rec_descriptor.bib_level)
112 AND (m.marc_vr_format IS NULL OR m.marc_vr_format = rec_descriptor.vr_format)
113 AND (m.ref_flag IS NULL OR m.ref_flag = item_object.ref)
116 CASE WHEN upgad.distance IS NOT NULL THEN 2^(2*weights.grp - (upgad.distance/denominator)) ELSE 0.0 END +
118 CASE WHEN ctoua.distance IS NOT NULL THEN 2^(2*weights.org_unit - (ctoua.distance/denominator)) ELSE 0.0 END +
119 CASE WHEN cnoua.distance IS NOT NULL THEN 2^(2*weights.copy_owning_lib - (cnoua.distance/denominator)) ELSE 0.0 END +
120 CASE WHEN iooua.distance IS NOT NULL THEN 2^(2*weights.copy_circ_lib - (iooua.distance/denominator)) ELSE 0.0 END +
121 CASE WHEN uhoua.distance IS NOT NULL THEN 2^(2*weights.user_home_ou - (uhoua.distance/denominator)) ELSE 0.0 END +
122 -- Circ Type -- Note: 4^x is equiv to 2^(2*x)
123 CASE WHEN m.is_renewal IS NOT NULL THEN 4^weights.is_renewal ELSE 0.0 END +
124 -- Static User Checks
125 CASE WHEN m.juvenile_flag IS NOT NULL THEN 4^weights.juvenile_flag ELSE 0.0 END +
126 CASE WHEN m.usr_age_lower_bound IS NOT NULL THEN 4^weights.usr_age_lower_bound ELSE 0.0 END +
127 CASE WHEN m.usr_age_upper_bound IS NOT NULL THEN 4^weights.usr_age_upper_bound ELSE 0.0 END +
128 -- Static Item Checks
129 CASE WHEN m.circ_modifier IS NOT NULL THEN 4^weights.circ_modifier ELSE 0.0 END +
130 CASE WHEN m.marc_type IS NOT NULL THEN 4^weights.marc_type ELSE 0.0 END +
131 CASE WHEN m.marc_form IS NOT NULL THEN 4^weights.marc_form ELSE 0.0 END +
132 CASE WHEN m.marc_vr_format IS NOT NULL THEN 4^weights.marc_vr_format ELSE 0.0 END +
133 CASE WHEN m.ref_flag IS NOT NULL THEN 4^weights.ref_flag ELSE 0.0 END DESC,
134 -- Final sort on id, so that if two rules have the same sorting in the previous sort they have a defined order
135 -- This prevents "we changed the table order by updating a rule, and we started getting different results"
138 -- Record the full matching row list
139 row_list := row_list || cur_matchpoint.id;
141 -- No matchpoint yet?
142 IF matchpoint.id IS NULL THEN
143 -- Take the entire matchpoint as a starting point
144 matchpoint := cur_matchpoint;
145 CONTINUE; -- No need to look at this row any more.
148 -- Incomplete matchpoint?
149 IF matchpoint.circulate IS NULL THEN
150 matchpoint.circulate := cur_matchpoint.circulate;
152 IF matchpoint.duration_rule IS NULL THEN
153 matchpoint.duration_rule := cur_matchpoint.duration_rule;
155 IF matchpoint.recurring_fine_rule IS NULL THEN
156 matchpoint.recurring_fine_rule := cur_matchpoint.recurring_fine_rule;
158 IF matchpoint.max_fine_rule IS NULL THEN
159 matchpoint.max_fine_rule := cur_matchpoint.max_fine_rule;
161 IF matchpoint.hard_due_date IS NULL THEN
162 matchpoint.hard_due_date := cur_matchpoint.hard_due_date;
164 IF matchpoint.total_copy_hold_ratio IS NULL THEN
165 matchpoint.total_copy_hold_ratio := cur_matchpoint.total_copy_hold_ratio;
167 IF matchpoint.available_copy_hold_ratio IS NULL THEN
168 matchpoint.available_copy_hold_ratio := cur_matchpoint.available_copy_hold_ratio;
170 IF matchpoint.renewals IS NULL THEN
171 matchpoint.renewals := cur_matchpoint.renewals;
173 IF matchpoint.grace_period IS NULL THEN
174 matchpoint.grace_period := cur_matchpoint.grace_period;
178 -- Check required fields
179 IF matchpoint.circulate IS NOT NULL AND
180 matchpoint.duration_rule IS NOT NULL AND
181 matchpoint.recurring_fine_rule IS NOT NULL AND
182 matchpoint.max_fine_rule IS NOT NULL THEN
183 -- All there? We have a completed match.
184 result.success := true;
187 -- Include the assembled matchpoint, even if it isn't complete
188 result.matchpoint := matchpoint;
190 -- Include (for debugging) the full list of matching rows
191 result.buildrows := row_list;
193 -- Hand the result back to caller
196 $func$ LANGUAGE plpgsql;
198 CREATE OR REPLACE FUNCTION action.find_hold_matrix_matchpoint(pickup_ou integer, request_ou integer, match_item bigint, match_user integer, match_requestor integer)
202 requestor_object actor.usr%ROWTYPE;
203 user_object actor.usr%ROWTYPE;
204 item_object asset.copy%ROWTYPE;
205 item_cn_object asset.call_number%ROWTYPE;
206 rec_descriptor metabib.rec_descriptor%ROWTYPE;
207 matchpoint config.hold_matrix_matchpoint%ROWTYPE;
208 weights config.hold_matrix_weights%ROWTYPE;
209 denominator NUMERIC(6,2);
211 SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
212 SELECT INTO requestor_object * FROM actor.usr WHERE id = match_requestor;
213 SELECT INTO item_object * FROM asset.copy WHERE id = match_item;
214 SELECT INTO item_cn_object * FROM asset.call_number WHERE id = item_object.call_number;
215 SELECT INTO rec_descriptor * FROM metabib.rec_descriptor WHERE record = item_cn_object.record;
217 -- The item's owner should probably be the one determining if the item is holdable
218 -- How to decide that is debatable. Decided to default to the circ library (where the item lives)
219 -- This flag will allow for setting it to the owning library (where the call number "lives")
220 PERFORM * FROM config.internal_flag WHERE name = 'circ.holds.weight_owner_not_circ' AND enabled;
222 -- Grab the closest set circ weight setting.
224 -- Default to circ library
225 SELECT INTO weights hw.*
226 FROM config.weight_assoc wa
227 JOIN config.hold_matrix_weights hw ON (hw.id = wa.hold_weights)
228 JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) d ON (wa.org_unit = d.id)
233 -- Flag is set, use owning library
234 SELECT INTO weights hw.*
235 FROM config.weight_assoc wa
236 JOIN config.hold_matrix_weights hw ON (hw.id = wa.hold_weights)
237 JOIN actor.org_unit_ancestors_distance( cn_object.owning_lib ) d ON (wa.org_unit = d.id)
243 -- No weights? Bad admin! Defaults to handle that anyway.
244 IF weights.id IS NULL THEN
245 weights.user_home_ou := 5.0;
246 weights.request_ou := 5.0;
247 weights.pickup_ou := 5.0;
248 weights.item_owning_ou := 5.0;
249 weights.item_circ_ou := 5.0;
250 weights.usr_grp := 7.0;
251 weights.requestor_grp := 8.0;
252 weights.circ_modifier := 4.0;
253 weights.marc_type := 3.0;
254 weights.marc_form := 2.0;
255 weights.marc_bib_level := 1.0;
256 weights.marc_vr_format := 1.0;
257 weights.juvenile_flag := 4.0;
258 weights.ref_flag := 0.0;
261 -- Determine the max (expected) depth (+1) of the org tree and max depth of the permisson tree
262 -- If you break your org tree with funky parenting this may be wrong
263 -- 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
264 -- We use one denominator for all tree-based checks for when permission groups and org units have the same weighting
265 WITH all_distance(distance) AS (
266 SELECT depth AS distance FROM actor.org_unit_type
268 SELECT distance AS distance FROM permission.grp_ancestors_distance((SELECT id FROM permission.grp_tree WHERE parent IS NULL))
270 SELECT INTO denominator MAX(distance) + 1 FROM all_distance;
272 -- To ATTEMPT to make this work like it used to, make it reverse the user/requestor profile ids.
273 -- This may be better implemented as part of the upgrade script?
274 -- Set usr_grp = requestor_grp, requestor_grp = 1 or something when this flag is already set
275 -- Then remove this flag, of course.
276 PERFORM * FROM config.internal_flag WHERE name = 'circ.holds.usr_not_requestor' AND enabled;
279 -- Note: This, to me, is REALLY hacky. I put it in anyway.
280 -- If you can't tell, this is a single call swap on two variables.
281 SELECT INTO user_object.profile, requestor_object.profile
282 requestor_object.profile, user_object.profile;
285 -- Select the winning matchpoint into the matchpoint variable for returning
286 SELECT INTO matchpoint m.*
287 FROM config.hold_matrix_matchpoint m
288 /*LEFT*/ JOIN permission.grp_ancestors_distance( requestor_object.profile ) rpgad ON m.requestor_grp = rpgad.id
289 LEFT JOIN permission.grp_ancestors_distance( user_object.profile ) upgad ON m.usr_grp = upgad.id
290 LEFT JOIN actor.org_unit_ancestors_distance( pickup_ou ) puoua ON m.pickup_ou = puoua.id
291 LEFT JOIN actor.org_unit_ancestors_distance( request_ou ) rqoua ON m.request_ou = rqoua.id
292 LEFT JOIN actor.org_unit_ancestors_distance( item_cn_object.owning_lib ) cnoua ON m.item_owning_ou = cnoua.id
293 LEFT JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) iooua ON m.item_circ_ou = iooua.id
294 LEFT JOIN actor.org_unit_ancestors_distance( user_object.home_ou ) uhoua ON m.user_home_ou = uhoua.id
297 -- AND (m.requestor_grp IS NULL OR upgad.id IS NOT NULL) -- Optional Requestor Group?
298 AND (m.usr_grp IS NULL OR upgad.id IS NOT NULL)
300 AND (m.pickup_ou IS NULL OR (puoua.id IS NOT NULL AND (puoua.distance = 0 OR NOT m.strict_ou_match)))
301 AND (m.request_ou IS NULL OR (rqoua.id IS NOT NULL AND (rqoua.distance = 0 OR NOT m.strict_ou_match)))
302 AND (m.item_owning_ou IS NULL OR (cnoua.id IS NOT NULL AND (cnoua.distance = 0 OR NOT m.strict_ou_match)))
303 AND (m.item_circ_ou IS NULL OR (iooua.id IS NOT NULL AND (iooua.distance = 0 OR NOT m.strict_ou_match)))
304 AND (m.user_home_ou IS NULL OR (uhoua.id IS NOT NULL AND (uhoua.distance = 0 OR NOT m.strict_ou_match)))
305 -- Static User Checks
306 AND (m.juvenile_flag IS NULL OR m.juvenile_flag = user_object.juvenile)
307 -- Static Item Checks
308 AND (m.circ_modifier IS NULL OR m.circ_modifier = item_object.circ_modifier)
309 AND (m.marc_type IS NULL OR m.marc_type = COALESCE(item_object.circ_as_type, rec_descriptor.item_type))
310 AND (m.marc_form IS NULL OR m.marc_form = rec_descriptor.item_form)
311 AND (m.marc_bib_level IS NULL OR m.marc_bib_level = rec_descriptor.bib_level)
312 AND (m.marc_vr_format IS NULL OR m.marc_vr_format = rec_descriptor.vr_format)
313 AND (m.ref_flag IS NULL OR m.ref_flag = item_object.ref)
316 CASE WHEN rpgad.distance IS NOT NULL THEN 2^(2*weights.requestor_grp - (rpgad.distance/denominator)) ELSE 0.0 END +
317 CASE WHEN upgad.distance IS NOT NULL THEN 2^(2*weights.usr_grp - (upgad.distance/denominator)) ELSE 0.0 END +
319 CASE WHEN puoua.distance IS NOT NULL THEN 2^(2*weights.pickup_ou - (puoua.distance/denominator)) ELSE 0.0 END +
320 CASE WHEN rqoua.distance IS NOT NULL THEN 2^(2*weights.request_ou - (rqoua.distance/denominator)) ELSE 0.0 END +
321 CASE WHEN cnoua.distance IS NOT NULL THEN 2^(2*weights.item_owning_ou - (cnoua.distance/denominator)) ELSE 0.0 END +
322 CASE WHEN iooua.distance IS NOT NULL THEN 2^(2*weights.item_circ_ou - (iooua.distance/denominator)) ELSE 0.0 END +
323 CASE WHEN uhoua.distance IS NOT NULL THEN 2^(2*weights.user_home_ou - (uhoua.distance/denominator)) ELSE 0.0 END +
324 -- Static User Checks -- Note: 4^x is equiv to 2^(2*x)
325 CASE WHEN m.juvenile_flag IS NOT NULL THEN 4^weights.juvenile_flag ELSE 0.0 END +
326 -- Static Item Checks
327 CASE WHEN m.circ_modifier IS NOT NULL THEN 4^weights.circ_modifier ELSE 0.0 END +
328 CASE WHEN m.marc_type IS NOT NULL THEN 4^weights.marc_type ELSE 0.0 END +
329 CASE WHEN m.marc_form IS NOT NULL THEN 4^weights.marc_form ELSE 0.0 END +
330 CASE WHEN m.marc_vr_format IS NOT NULL THEN 4^weights.marc_vr_format ELSE 0.0 END +
331 CASE WHEN m.ref_flag IS NOT NULL THEN 4^weights.ref_flag ELSE 0.0 END DESC,
332 -- Final sort on id, so that if two rules have the same sorting in the previous sort they have a defined order
333 -- This prevents "we changed the table order by updating a rule, and we started getting different results"
336 -- Return just the ID for now
337 RETURN matchpoint.id;
339 $func$ LANGUAGE 'plpgsql';