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